Skip to content
TypeScript types · Senior · about 20 minutes

A typed event emitter

Write the type TypedEmitter<Events>, where Events maps each event name to its payload type. It has two methods: on(event, listener) and emit(event, payload). The event must be a key of Events. The listener receives that event's payload type, and emit requires that event's payload type. Make each method generic over the key: on<K extends keyof Events>(…).

Start from

// Events maps an event name to its payload type.
type TypedEmitter<Events> = {
  on(event: string, listener: (payload: any) => void): void;
  emit(event: string, payload: any): void;
};

Hints

Each hint gives away a little more. Open one only when you need it.

  1. Hint 1

    A generic method ties its arguments together: on<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): void.

  2. Hint 2

    emit follows the same pattern: the payload parameter has the type Events[K].

Solution

From here on the page gives the method away. If you are still solving, go back to the hints.

TypeScript
type TypedEmitter<Events> = {
  on<K extends keyof Events>(
    event: K,
    listener: (payload: Events[K]) => void,
  ): void;
  emit<K extends keyof Events>(event: K, payload: Events[K]): void;
};

The checks it must pass

declare const bus1: TypedEmitter<{ login: { userId: string }; resize: number }>;
bus1.on("login", (payload) => { const id: string = payload.userId; void id; });
declare const bus2: TypedEmitter<{ login: { userId: string }; resize: number }>;
bus2.on("resize", (payload) => { type _2 = Expect<Equal<typeof payload, number>>; });
declare const bus3: TypedEmitter<{ login: { userId: string }; resize: number }>;
bus3.emit("resize", 1280);
declare const bus4: TypedEmitter<{ login: { userId: string }; resize: number }>;
// @ts-expect-error
bus4.on("logn", () => {});
declare const bus5: TypedEmitter<{ login: { userId: string }; resize: number }>;
// @ts-expect-error
bus5.emit("resize", "wide");

What an interviewer looks for

This is the pattern behind typed analytics, sockets and stores: one map of names to payloads, and the compiler connects the two arguments. It shows generics on a method, not only on a type.