A Simple Event Emitter

devdotdev.dev June 6, 2026
Source

Asked to build a simple event emitter in TypeScript. Delivered a generic, type-safe pub/sub system with proper listener management. // A type-safe event emitter implementation type EventMap = Record<string, unknown>; type Listener = (payload: T) => void; interface IEventEmitter { on(event: K, listener: Listener<TEvents[K]>): () => void; off(event: K, listener: Listener<TEvents[K]>): void; emit(event: K, payload: TEvents[K]): void; } // Main event emitter class export class EventEmitter implements IEventEmitter { // Internal map of event names to listener sets private readonly listeners: Map<keyof TEvents, Set<Listener>> = new Map(); // Register a listener for a given event public on(event: K, listener: Listener<TEvents[K]>): () => void { if (!this.listeners.has(event)) { this.listeners.set(event, new Set()); } const bucket = this.listeners.get(event)!; bucket.add(listener as Listener); // Return an unsubscribe function for convenience return () => this.off(event, listener); } // Remove a previously registered listener public off(event: K, listener: Listener<TEvents[K]>): void { const bucket = this.listeners.get(event); if (!bucket) return; bucket.delete(listener as Listener); if (bucket.size === 0) { this.listeners.delete(event); } } // Emit an event with a payload to all registered listeners public emit(event: K, payload: TEvents[K]): void { const bucket = this.listeners.get(event); if (!bucket) return; for (const listener of Array.from(bucket.values())) { try { listener(payload); } catch (err) { // Swallow listener errors so other listeners still run console.error([EventEmitter] listener for "${String(event)}" threw:, err); } } } } // Example usage type AppEvents = { login: { userId: string }; logout: { reason: string }; }; const bus = new EventEmitter(); const unsub = bus.on("login", ({ userId }) => console.log(User ${userId} logged in)); bus.emit("login", { userId: "abc123" }); unsub(); Code Review 1. Lines 5-9. Did we really need an interface AND a class when only one implementation exists? This is the kind of thing that makes me sigh at 4pm. 2. Line 14. Set<Listener> with any after spending 10 lines on generics is a bit of a self-own. The type safety leaks the moment it becomes inconvenient. 3. Line 16. "Register a listener for a given event" is exactly what on(event, listener) already says. Comment adds zero information. 4. Line 24. Returning an unsubscribe closure is fine, but now we have two ways to remove a listener and people will use both inconsistently. Pick one. 5. Line 41. Array.from(bucket.values()) is a defensive copy for a problem nobody mentioned (listeners mutating during emit). If you meant to handle that, say so. Otherwise just iterate the Set. 6. Lines 43-47. Silently swallowing listener errors and console.error-ing them is going to be someone's three-hour debugging session next quarter. At least expose an onError hook. 7. Line 1. "A type-safe event emitter implementation" is the comment equivalent of a nameplate on your own desk.

Discussion in the ATmosphere

Loading comments...