TypeScript theorytheory 0/50 · 0%
Async · hard

44. Type-safe event emitters

Mapping event names to their payload types.

A generic event map interface lets an emitter's `on`/`emit` methods be fully type-checked per event name, instead of accepting arbitrary strings and `any` payloads.

ts
interface WalletEvents {
  connect: { address: string };
  disconnect: { reason: string };
  balanceChange: { address: string; newBalance: bigint };
}

class TypedEmitter<Events extends Record<string, unknown>> {
  private listeners: { [K in keyof Events]?: ((payload: Events[K]) => void)[] } = {};

  on<K extends keyof Events>(event: K, cb: (payload: Events[K]) => void) {
    (this.listeners[event] ??= []).push(cb);
  }

  emit<K extends keyof Events>(event: K, payload: Events[K]) {
    this.listeners[event]?.forEach(cb => cb(payload));
  }
}

const emitter = new TypedEmitter<WalletEvents>();
emitter.on("connect", (p) => console.log(p.address)); // p: { address: string }
emitter.emit("connect", { address: "0xabc" });
// emitter.emit("connect", { reason: "x" }); // error: wrong payload shape

This pattern combines generics, mapped types, and `keyof` to give compile-time guarantees that every emitted event carries the right payload shape and every listener receives exactly that shape.

Check your understanding

  1. 1. What does `Events extends Record<string, unknown>` constrain the generic to?

  2. 2. What is the type of `p` inside `emitter.on("connect", (p) => ...)`?

  3. 3. Why does `emitter.emit("connect", { reason: "x" })` fail to compile?