Home > front end >  How to strongly type an event emitter such that the handler's parameter type is inferred from t
How to strongly type an event emitter such that the handler's parameter type is inferred from t

Time:01-02

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}`);
}
  • Related