If you wish to maintain the exact structure of MyEvent
, take a look at this example:
type UnionKeys<T> = T extends T ? keyof T : never;
// credits to https://stackoverflow.com/questions/65805600/type-union-not-checking-for-excess-properties#answer-65805753
type StrictUnionHelper<T, TAll> =
T extends any
? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;
type StrictUnion<T> = StrictUnionHelper<T, T>
type MyEvent = StrictUnion<
| {
eventA: {
foo: number;
bar: number;
};
}
| {
eventB: {
baz: string;
};
}>;
type Handler = {
[K in keyof MyEvent]: (data: NonNullable<MyEvent[K]>) => void;
};
const handler: Handler = {
eventA: (data) => {
data.bar // ok
data.foo // ok
},
eventB: (data) => {
data.baz // ok
}
};
Playground
However, I don't like that I was compelled to use NonNullable
. It appears to be somewhat of a workaround. You can actually extract all keys using distributive conditional types as shown below:
type MyEvent =
| {
eventA: {
foo: number;
bar: number;
};
}
| {
eventB: {
baz: string;
};
};
type Keys<T> = T extends Record<infer Key, infer _> ? Key : never;
type Values<T> = T[keyof T]
type Handler = {
[K in Keys<MyEvent>]: (data: Values<Extract<MyEvent, Record<K, unknown>>>) => void;
};
const handler: Handler = {
eventA: (data) => {
data.bar // ok
data.foo // ok
},
eventB: (data) => {
data.baz // ok
}
};
Playground
Please bear in mind that there is a simpler approach available. It might be worth considering creating a mapped data structure, similar to the one mentioned here:
type MyEvent = {
eventA: {
foo: number;
bar: number;
},
eventB: {
baz: string;
}
}
type Handler = {
[K in keyof MyEvent]: (data: MyEvent[K]) => void;
};
const handler: Handler = {
eventA: (data) => {
data.bar // ok
data.foo // ok
},
eventB: (data) => {
data.baz // ok
}
};
You may find this example interesting as well