typescript
46 lines · 7 steps
A type-safe event bus in TypeScript
A generic MessageBus maps event names to handler sets, keeping payload types in sync with each event key.
Explained by
highlit
1type EventMap = Record<string, unknown>;
2
3type Handler<T> = (payload: T) => void;
4
5export class MessageBus<Events extends EventMap> {
6 private handlers = new Map<keyof Events, Set<Handler<unknown>>>();
7
8 on<K extends keyof Events>(event: K, handler: Handler<Events[K]>): () => void {
9 let set = this.handlers.get(event);
10 if (!set) {
11 set = new Set();
12 this.handlers.set(event, set);
13 }
14 set.add(handler as Handler<unknown>);
15 return () => this.off(event, handler);
16 }
17
18 once<K extends keyof Events>(event: K, handler: Handler<Events[K]>): () => void {
19 const wrapped: Handler<Events[K]> = (payload) => {
20 off();
21 handler(payload);
22 };
23 const off = this.on(event, wrapped);
24 return off;
25 }
26
27 off<K extends keyof Events>(event: K, handler: Handler<Events[K]>): void {
28 const set = this.handlers.get(event);
29 if (!set) return;
30 set.delete(handler as Handler<unknown>);
31 if (set.size === 0) this.handlers.delete(event);
32 }
33
34 emit<K extends keyof Events>(event: K, payload: Events[K]): void {
35 const set = this.handlers.get(event);
36 if (!set) return;
37 for (const handler of [...set]) {
38 (handler as Handler<Events[K]>)(payload);
39 }
40 }
41
42 clear(event?: keyof Events): void {
43 if (event === undefined) this.handlers.clear();
44 else this.handlers.delete(event);
45 }
46}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Indexed access types like Events[K] let one generic parameter enforce that every event name carries its matching payload type.
- 2Storing subscriptions in a Set and returning an unsubscribe closure gives callers a clean, self-contained teardown handle.
- 3Iterating over a snapshot copy of listeners protects against mutation while handlers fire.
Related explainers
typescript
import { Injectable, computed, inject, signal } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { firstValueFrom } from 'rxjs';
Optimistic updates with Angular signals
signals
optimistic-updates
state-management
Intermediate
9 steps
go
package middleware import ( "crypto/subtle"
Building an API-key middleware in Gin
middleware
authentication
dependency-injection
Intermediate
5 steps
typescript
export class PriorityQueue<T> { private heap: Array<{ value: T; priority: number }> = []; get size(): number {
Building a binary-heap priority queue in TypeScript
binary-heap
priority-queue
generics
Intermediate
9 steps
javascript
class EventEmitter { constructor() { this.listeners = new Map(); }
Building an EventEmitter in JavaScript
pub-sub
closures
map
Intermediate
7 steps
typescript
import { Injectable, signal, computed } from '@angular/core'; import { HttpInterceptorFn, HttpContextToken,
Tracking HTTP loading state in Angular
signals
http-interceptor
reference-counting
Intermediate
8 steps
typescript
type Event = Record<string, unknown>; interface BatcherOptions { endpoint: string;
Batching analytics events in TypeScript
batching
buffering
async
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/a-type-safe-event-bus-in-typescript-explained-typescript-c0f4/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.