Let's take a look at a basic event emitter in Javascript.
const emitter = new EventEmitter()
emitter.on('message', (payload) => {
console.log(payload)
// { "body": "hello" }
})
emitter.send('message', { body: 'hello' })
We can both send events and listen for events. But depending on the event name we'll receive a different payload or no payload at all.
How can we add types which change depending on the event name? The trick is the keyof
type operator.
Let's start with the send
method.
class EventEmitter<Events> {
send<Key extends keyof Events>(event: Key, data: Events[Key]) {
// implement...
}
}
First we define a generic Events
which represends all possible event types for our emitter. For the send
method we then define a second generic Key
which represents the keys of Events
.
We would pass the event types when initializing the emitter.
type EventTypes = {
message: { body: string }
connect: { id: number }
initialize: undefined
}
const emitter = new EventEmitter<EventTypes>()
With this we can only supply message
, connect
or initialize
for the first value of send
and the data will also be mapped correctly.
We can see that we must pass { body: string }
when the event type is message
We can then follow the same logic for the on
method except this time we need to handle a callback.
class EventEmitter<Events> {
on<Key extends keyof Events>(
event: Key,
callback: (data: Events[Key]) => void
) {
// implement...
}
send<Key extends keyof Events>(event: Key, data: Events[Key]) {
// implement...
}
}
This gets us pretty far. But what if we don't want to send any data at all? Currently for the initialize
event we would still need to send undefined
otherwise we get a type error.
To fix this we need to use a combination of typescript conditional types and the spread syntax.
class EventEmitter<Events> {
send<Key extends keyof Events>(
event: Key,
...data: Events[Key] extends undefined ? [] : [Events[Key]]
) {
// implement...
}
}
Here we say if the event value is undefined the data parameter is not required, otherwise it's Events[Key]
The reason we use spread syntax is because spreading an empty array results in nothing.
Repeat the same logic for the on
method and we end up with a final typed emitter.
class EventEmitter<Events> {
on<Key extends keyof Events>(
event: Key,
callback: (
...data: Events[Key] extends undefined ? [] : [Events[Key]]
) => void
) {
// implement...
}
send<Key extends keyof Events>(
event: Key,
...data: Events[Key] extends undefined ? [] : [Events[Key]]
) {
// implement...
}
}