Search code examples
genericstypescripteventemitter

Typed events with TypeScript and EventEmitter


I'm trying to add strongly typed events to an EventEmitter-like system using TypeScript.

Currently, we define our types like:

interface TypedMsg<Name, T> {
  messageType: Name;
  message: T;
}

type TypedMsgFoo = TypedMsg<'FOO', string>;
type TypedMsgBar = TypedMsg<'BAR', number>;
type EitherFooOrBar = TypedMsgFoo | TypedMsgBar;

I would like to define an interface like:

interface EventHandler<T extends TypedMsg<any, any> {
  on: (messageType: T.messageType, handler: (T.message) => void) => void;
}

But Typescript doesn't support extracting subtypes like T.messageType. Is there another way to do this?

The end goal would be to define handlers with proper typing with just:

class FooBarHandler implements EventHandler<EitherFooOrBar> {
  on(messageType: EitherFooOrBar.messageType) {...}
}

Solution

  • Typescript does support extracting types of members, just the syntax is a bit unusual - it's called indexed access type operator

    interface TypedMsg<Name, T> {
      messageType: Name;
      message: T;
    }
    
    type TypedMsgFoo = TypedMsg<'FOO', string>;
    type TypedMsgBar = TypedMsg<'BAR', number>;
    type EitherFooOrBar = TypedMsgFoo | TypedMsgBar;
    
    interface EventHandler<T extends TypedMsg<{}, {}>> {
      on: (messageType: T['messageType'], handler: (m: T['message']) => void) => void;
    }
    
    class FooBarHandler implements EventHandler<EitherFooOrBar> {
        on(
            messageType: EitherFooOrBar['messageType'], 
            handler: (m: EitherFooOrBar['message']) => void
        ) {
    
        }
    }
    

    However it will soon become pretty tedious to type all these declarations with types explicitly spelled out - you will want do devise something that will allow typescript to infer types for you, for example something like this question: TypeScript type inference/narrowing challenge