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.
Useful resources
Or clone locally
git clone -b challenge/2026-09-13 https://github.com/niltonheck/typedrop.git