TypeDrop

2026-08-16 Challenge

2026-08-16 Medium

Typed Event Emitter with Discriminated Payloads

You're building the real-time notification hub for a collaborative document editor. Components across the app subscribe to strongly-typed events (cursor moves, edits, presence changes, errors) — the emitter must guarantee that every listener receives exactly the payload shape its event name promises, with no casting and no missed cases.

Goals

  • Implement TypedEmitter<TMap> so that emit, on, once, off, and listenerCount are fully type-safe — the compiler must infer the correct payload type from the event name alone.
  • Implement replayLast so that a late subscriber immediately receives the most-recently emitted payload synchronously, while storing the cache on the emitter instance without polluting its public API.
  • Implement mergeEmitters so that a single returned emitter transparently re-emits events from all source emitters and can also be used as a standalone emitter.
  • Ensure the entire file compiles under strict: true with zero use of `any`, `as`, or type assertions.
challenge.ts
// Key types and main class signature

export interface EventMap {
  [event: string]: unknown;
}

export type Listener<TPayload> = (payload: TPayload) => void;
export type Unsubscribe = () => void;

export interface DocEventMap extends EventMap {
  cursorMoved:      CursorMovedPayload;
  textEdited:       TextEditedPayload;
  presenceChanged:  PresenceChangedPayload;
  docError:         DocErrorPayload;
}

export class TypedEmitter<TMap extends EventMap> {
  emit<K extends keyof TMap>(event: K, payload: TMap[K]): void;
  on<K extends keyof TMap>(event: K, listener: Listener<TMap[K]>): Unsubscribe;
  once<K extends keyof TMap>(event: K, listener: Listener<TMap[K]>): Unsubscribe;
  off<K extends keyof TMap>(event: K, listener: Listener<TMap[K]>): void;
  listenerCount<K extends keyof TMap>(event: K): number;
}
Hints (click to reveal)

Hints

  • For the listener store, a `Map<keyof TMap, Set<Listener<...>>>` won't directly work because of the index type mismatch — consider how `Map<string | symbol, Set<Listener<unknown>>>` combined with a cast-free accessor helper can satisfy the compiler.
  • For `replayLast`, a `WeakMap<TypedEmitter<TMap>, Map<K, TMap[K]>>` keyed on the emitter instance lets you store last-payload state without touching the class or using `any`.
  • For `mergeEmitters`, subscribe to every event key of each source emitter — you'll need to iterate `Object.keys` or keep a known set of keys, then forward each emission to the merged emitter via its own `emit`.

Or clone locally

git clone -b challenge/2026-08-16 https://github.com/niltonheck/typedrop.git