I'm trying to create a type for an event handler to have a working autocomplete for the data of the events. The events I want to handle are structured like this:
type MyEvent =
| {
eventA: {
foo: number;
bar: number;
};
}
| {
eventB: {
baz: string;
};
};
This is what I have for the handler so far:
type UnionKeys<T> = T extends T ? keyof T : never;
type Handler = {
[K in UnionKeys<MyEvent>]: (data: /* ??? */) => void;
};
const handler: Handler = {
eventA: (data) => console.log(data.foo), // I would like to have working autocomplete for data.* here
eventB: (data) => console.log(data.baz),
};
I'm not sure how to construct a type for 'data' in the Handler type to match the event. Any ideas?
CodePudding user response:
If you want to stick with exact structure of MyEvent
, consider this example:
type UnionKeys<T> = T extends T ? keyof T : never;
// credits goes 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
}
};
However, I don't like that I was forced to use NonNullable
. It seems to be a hack. It is possible to obtain all keys with help of distributive conditional types:
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
}
};
Please keep in mind, that there is an easier approach. I believe it worth creating mapped data structure, just like 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 might be interested in this example also