Become a GraphQL expert

We're launching a brand new course. Pre-sale is now live.

View course

Overstacked

Wed May 01 2024

Add Type Support to Event Emitters

Event emitters use the same method to handle all event types. So how do you add different types depending on the event?

cover image

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.

Send event types

We can see that we must pass { body: string } when the event type is message

Send type error

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.

Send 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...
  }
}

You can get more actionable ideas in my popular email newsletter. Each week, I share deep dives like this, plus the latest product updates. Join over 80,000 developers using my products and tools. Enter your email and don't miss an update.

You'll stay in the loop with my latest updates. Unsubscribe at any time.

© Copyright 2024 Overstacked. All rights reserved.

Created by Warren Day