I'm trying to define a strongly typed event-emitter, what I mostly want is to have the callback's event type inferred from the string passed to the addEventHandler
function.
But I've failed so far, and what I came up with infers the event type from the callback, not the opposite.
Here's an example (with a fiddle):
interface NumberEvent {
type: 'NumberEvent';
num: number;
}
interface StringEvent {
type: 'StringEvent';
str: string;
}
type AnyEvent = NumberEvent | StringEvent;
const addEventHandler = <ET extends AnyEvent>(type: ET['type'], handler: ((event: ET) => void)) => {
console.log(`added event handler for ${type}`);
}
addEventHandler('NumberEvent', (event: NumberEvent) => {
// this is cool
});
addEventHandler('NumberEvent', (event: StringEvent) => {
// this doesn't type check, good
});
addEventHandler('type does not exist', (x: any) => {
// why no type error?
});
I do not understand why the last line type-checks, because there is no instance of AnyEvent
with type 'type does not exist'
.
Can you think of a better approach to the problem?
CodePudding user response:
You can achieve this by making addEventHandler
generic on the event type, rather than the event object.
const addEventHandler = <ET extends AnyEvent['type']>(
type: ET,
handler: ((event: Extract<AnyEvent, { type: ET }>) => void)
) => {
console.log(`added event handler for ${type}`);
}
You could also use AnyEvent & { type: ET }
instead of Extract<AnyEvent & { type: ET }
The reason your type doesn't prevent the last case is because ET
is inferred as any
. any["type"]
is still any
, so it will allow any string at all.
The above version still won't prevent someone from doing this:
addEventHandler<any>('type does not exist', (x: any) => {
// explicitly providing <any>
});
You can prevent this, by using the fact that <anything> & any
is any
, but personally I wouldn't bother. Nobody is likely to provide any
here unless intentionally trying to break your types. With the any
check, you can also go back to your generic:
type NotAny<T> = 0 extends (1 & T) ? never : T;
const addEventHandler = <ET extends AnyEvent>(type: NotAny<ET["type"]>, handler: ((event: ET) => void)) => {
console.log(`added event handler for ${type}`);
}