TypeDrop

2026-09-27 Challenge

2026-09-27 Medium

Typed Event Emitter with Middleware Pipeline

You're building the core event bus for a real-time collaboration tool. Components emit strongly-typed events, middleware can transform or gate events before they reach subscribers, and every handler is guaranteed by the compiler to receive the exact payload shape for its event.

Goals

  • Implement `on()` so handlers are stored per-event and the returned unsubscribe function removes only that handler.
  • Implement `once()` in terms of `on()` so the handler fires at most once then auto-removes itself.
  • Implement `use()` to register universal middleware in order and return `this` for chaining.
  • Implement `emit()` to run middleware in sequence (allowing payload transformation or cancellation) then invoke all registered handlers, returning a fully-populated `EmitRecord<E>`.
challenge.ts
// Key types & main class signature

interface AppEventMap {
  "user:joined":     { userId: string; roomId: string; timestamp: number };
  "user:left":       { userId: string; roomId: string; reason: "timeout" | "disconnect" | "kicked" };
  "message:sent":    { messageId: string; authorId: string; body: string; roomId: string };
  "message:deleted": { messageId: string; deletedBy: string };
  "room:created":    { roomId: string; ownerId: string; maxCapacity: number };
}

type EventName = keyof AppEventMap;
type Payload<E extends EventName> = AppEventMap[E];
type EmitStatus = "delivered" | "cancelled" | "no-listeners";

type AnyMiddleware = <E extends EventName>(
  event: E, payload: Payload<E>
) => Payload<E> | null | undefined | Promise<Payload<E> | null | undefined>;

class TypedEventEmitter {
  on<E extends EventName>(event: E, handler: (p: Payload<E>) => void): () => void { ... }
  once<E extends EventName>(event: E, handler: (p: Payload<E>) => void): () => void { ... }
  use(middleware: AnyMiddleware): this { ... }
  async emit<E extends EventName>(event: E, payload: Payload<E>): Promise<EmitRecord<E>> { ... }
  listenerCount<E extends EventName>(event: E): number { ... }
}
Hints (click to reveal)

Hints

  • The `handlers` map stores `Handler<EventName>` but you're registering `Handler<E>` — think about how to safely cast at the storage boundary without using `as` or `any` on public-facing types.
  • For `once()`, capture the unsubscribe function returned by `on()` inside a wrapper handler and call it before (or after) invoking the original handler.
  • In `emit()`, use a `for...of` loop over `this.middlewares` with `await` so async middleware are handled correctly; track the count with a local variable that increments only when the middleware returns a non-null result.

Or clone locally

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