TypeDrop

2026-09-13 Challenge

2026-09-13 Medium

Typed In-Memory Event Bus with Subscription Management

You're building the real-time notification core for a collaborative document editor. Components across the app publish and subscribe to strongly-typed events (cursor moves, document edits, user presence changes); the event bus must guarantee that every subscriber receives exactly the payload shape its event defines — and the compiler must reject any mismatched publish or subscribe call.

Goals

  • Define `EventName`, `EventPayload<K>`, and `EventHandler<K>` entirely from `AppEventMap` using mapped-type indexing and `keyof` — no hard-coded strings.
  • Implement `on`, `off`, `emit`, and `listenerCount` on the returned bus so subscriptions are tracked, payloads are type-safe, and removal is accurate.
  • Implement `once` so the handler auto-unsubscribes after its first invocation and its token can also be cancelled early via `off`.
  • Keep the internal subscription registry fully typed — no `any`, no unsafe casts, no index-signature hacks.
challenge.ts
// Key types you'll work with:

export interface AppEventMap {
  "cursor:move": { userId: string; x: number; y: number };
  "doc:edit":    { userId: string; delta: string; timestamp: number };
  "user:join":   { userId: string; displayName: string };
  "user:leave":  { userId: string };
  "doc:save":    { docId: string; savedBy: string; version: number };
}

export type EventName = /* TODO: derived from AppEventMap */ never;
export type EventPayload<K extends EventName> = /* TODO */ never;
export type EventHandler<K extends EventName> = /* TODO */ never;

export type SubscriptionToken = string & { readonly __brand: "SubscriptionToken" };

// Main factory you must implement:
export function createEventBus(): IEventBus { /* TODO */ }
Hints (click to reveal)

Hints

  • The internal store can be a `Map` keyed by `EventName`; the value for each key can be an array of a small interface like `{ token: SubscriptionToken; handler: EventHandler<K>; once: boolean }` — but you'll need a mapped type or a union to keep `K` in sync with the key.
  • For the registry, consider storing entries as `{ token: SubscriptionToken; handler: (payload: AppEventMap[EventName]) => void; once: boolean }` keyed by token in a flat `Map<SubscriptionToken, …>` alongside a reverse index from event name to token set — this sidesteps the per-K generic storage problem.
  • To create a `SubscriptionToken` without using `as`, write a tiny private helper like `function makeToken(): SubscriptionToken { return crypto.randomUUID() as SubscriptionToken; }` — this is the one place a cast is acceptable since it's an internal factory.

Or clone locally

git clone -b challenge/2026-09-13 https://github.com/niltonheck/typedrop.git