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