Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to strongly type an event emitter such that the handler's parameter type is inferred from the event type?

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?

like image 359
djfm Avatar asked Dec 06 '25 02:12

djfm


1 Answers

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}`);
}
like image 146
Gerrit0 Avatar answered Dec 10 '25 15:12

Gerrit0



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!