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.
Lets break this down:
Type is a generic, that is inferred from name, and extends EmitterEvents[‘type’]
:
on<Type extends EmitterEvents['type']>(name: Type /*...*/)
Because
Type
extendsEmitterEvents['type'],
it can only bea
, orb
It can only be a or b, because EmitterEvents is a combination of AEvent and BEvent.
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.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:
Extract takes a Union type, lets call it
h2o
, and a second type, which we’ll callfilter.
Extract will return a new union type, where all the first types in our Union Type
h2o
, much match the second typefilter.
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:
a generic
Type
, which must be one of the type strings specified in the union typeEmitterEvents
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”
.We have a callback which accepts an argument
evt
. The type forevt
is determined using Extract.Extract takes all the
EmitterEvents
(which is a union of severalEmitterEvent
types) and filters them down to just the the EmitterEvents which have thetype
property matching ourname
argument (which is of generic typeType).
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.
And if you’re really digging it, share the whole 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