Challenge 3: Build a Typed Event Bus — Possible Solution ==================================================================== interface AppEvents { "order.placed": { orderId: string; total: number }; "order.cancelled": { orderId: string; reason: string }; "user.registered": { userId: string; email: string }; } class TypedEventBus { private listeners: { [K in keyof Events]?: Array<(payload: Events[K]) => void>; } = {}; on(event: K, listener: (payload: Events[K]) => void): void { const existing = this.listeners[event] ?? []; existing.push(listener); this.listeners[event] = existing; } off(event: K, listener: (payload: Events[K]) => void): void { const existing = this.listeners[event]; if (!existing) return; this.listeners[event] = existing.filter((l) => l !== listener) as typeof existing; } emit(event: K, payload: Events[K]): void { this.listeners[event]?.forEach((listener) => listener(payload)); } } // --- Usage --- const bus = new TypedEventBus(); bus.on("order.placed", (payload) => { console.log(`Order ${payload.orderId} placed for $${payload.total}`); }); bus.on("user.registered", (payload) => { console.log(`New user ${payload.userId} (${payload.email})`); }); bus.emit("order.placed", { orderId: "o1", total: 49.99 }); // ✅ compiles bus.emit("user.registered", { userId: "u1", email: "a@b.com" }); // ✅ compiles // The following would NOT compile — demonstrating the type safety: // bus.emit("order.placed", { orderId: "o1" }); // ❌ missing "total" // bus.emit("order.plcaed", { orderId: "o1", total: 10 }); // ❌ "order.plcaed" not in AppEvents // bus.on("order.placed", (payload) => { payload.reason; }); // ❌ "reason" doesn't exist on order.placed's payload WHY THIS WORKS -------------- - The `Events` generic parameter is a plain interface mapping event names to their payload shapes — `AppEvents` here — with no runtime representation; it exists purely to constrain `on`/`off`/`emit` at compile time. - `K extends keyof Events` ties the event-name argument directly to `Events[K]`, the payload type for that specific event — so the payload parameter's type changes automatically based on which event name was passed, without any manual overloading. - The mapped type `{ [K in keyof Events]?: Array<(payload: Events[K]) => void> }` gives `listeners` a correctly-typed array of listeners per event key, so `emit` can call each listener with a payload whose type it can already prove matches what the listener expects.