Type Safe Event Emitter Pattern

A clear example of a basic pattern to keep your event emitters typed.

If you’ve ever wanted to implement type safety with your event emitters, you might wonder how to emit or accept types based on the string for each event.

TS Playground Link


Lets break this down:

Type is a generic, that is inferred from name, and extends EmitterEvents[‘type’]:

on<Type extends EmitterEvents['type']>(name: Type /*...*/)
  1. Because Type extends EmitterEvents['type'], it can only be a, or b

    1. It can only be a or b, because EmitterEvents is a combination of AEvent and BEvent.

    2. AEvent and BEvent extend EmitterEvent, which takes a string as an generic argument, and specifies that this exact string, must the property `type` for any object with this interface.

    3. Because EmitterEvents is composed of two interfaces, which have constant type properties, and the type properties are known at compile time, typescript can infer that the only possible values for Type extends EmitterEvents['type'] are “a” or “b”.

Now that we have determined that the Generic Type from line 21, can only be “a” or “b”, we can start to break down how Extract<EmitterEvents, {type: Type}> works:

  1. Extract takes a Union type, lets call it h2o, and a second type, which we’ll call filter.

  2. Extract will return a new union type, where all the first types in our Union Type h2o, much match the second type filter.

  3. That is kind of a brain teaser when written in English, but lets look at it in code. Typescript Playground Link

We’re really getting into the weeds now! I hope you sharpened your machete! But don’t worry, we’re almost there. We just need to put it all together:

on<Type extends EmitterEvents['type']>(name: Type, callback: (evt: Extract<EmitterEvents, {type: Type}>) => void) {
        return super.on(name, callback)
 }

So what we have here, is:

  1. a generic Type, which must be one of the type strings specified in the union type EmitterEvents

  2. we have an argument called name, that must be of type Type, which will be a single string. In this example, it must be “a” or “b”.

  3. We have a callback which accepts an argument evt. The type for evt is determined using Extract.

  4. Extract takes all the EmitterEvents (which is a union of several EmitterEvent types) and filters them down to just the the EmitterEvents which have the type property matching our name argument (which is of generic type Type).

  5. Finally, it all works.

Finally, we have it all figured out. We can apply the same login to emit that we did to on. Not convinced? Try it out on the typescript playground.

This article is a bit of an experiment.

I’m giving substack a shot. So if you liked this content, and would like to see more, let me know by subscribing.

If you really want found this useful, the best way to give back is to share this post.

Share

And if you’re really digging it, share the whole newsletter.

Share Eric’s Newsletter

Does the idea of subcribing make you angry? Are you horribly offended? Maybe you think I can’t write worth a damn?! Is this all just convoluted nonsense? Or even worse, you spooted a teapo?!?! Let me know

Leave a comment