← All tasksTypeScript · Middle

Typed EventEmitter

The Emitter<Events> class gets a description of its events: new Emitter<{ login: { userId: number }; message: string }>(). on(event, handler) subscribes a handler and returns an unsubscribe function, emit(event, payload) calls the handlers of that event.

Make the types precise: an event name can only be one of the keys of Events, and the payload type in the handler and in emit comes from that particular event.

Tests (7)

Type tests are checked by the TypeScript compiler in strict mode. Expect<Equal<A, B>> passes when the types match exactly, and // @ts-expect-error passes when the next line does not compile. The first run downloads the compiler, so it takes a few seconds.

  1. types
    type E = { login: { userId: number }; message: string };
    const e = new Emitter<E>();
    e.on('login', (payload) => {
      type _ = Expect<Equal<typeof payload, { userId: number }>>;
    });
  2. types
    type E = { login: { userId: number }; message: string };
    const e = new Emitter<E>();
    // @ts-expect-error - message carries a string
    e.emit('message', 42);
  3. types
    type E = { login: { userId: number }; message: string };
    const e = new Emitter<E>();
    // @ts-expect-error - there is no such event
    e.on('signup', () => {});
  4. const e = new Emitter();
    const a = fn();
    const b = fn();
    e.on('message', a);
    e.on('message', b);
    e.emit('message', 'hi');
    expect(a).toHaveBeenCalledWith('hi');
    expect(b).toHaveBeenCalledWith('hi');
  5. const e = new Emitter();
    const h = fn();
    const off = e.on('login', h);
    e.emit('login', { userId: 1 });
    off();
    e.emit('login', { userId: 2 });
    expect(h).toHaveBeenCalledTimes(1);
    expect(h).toHaveBeenLastCalledWith({ userId: 1 });
  6. const e = new Emitter();
    expect(() => e.emit('nobody-listens', 1)).not.toThrow();
  7. const e = new Emitter();
    const a = fn();
    const b = fn();
    e.on('x', a);
    e.on('y', b);
    e.emit('x', 1);
    expect(a).toHaveBeenCalledTimes(1);
    expect(b).toHaveBeenCalledTimes(0);