BehavioralTypeScriptverifiedVerified
Observer Pattern in TypeScript
Defines a one-to-many dependency so that when one object changes state, all its dependents are notified and updated automatically.
How to Implement the Observer Pattern in TypeScript
1Step 1: Define the event listener type
type Listener<T> = (event: T) => void;2Step 2: Create the EventEmitter class with subscribe/unsubscribe
class EventEmitter<T> {
private listeners: Listener<T>[] = [];
subscribe(listener: Listener<T>): () => void {
this.listeners.push(listener);
return () => this.unsubscribe(listener);
}
unsubscribe(listener: Listener<T>): void {
this.listeners = this.listeners.filter(l => l !== listener);
}
emit(event: T): void {
for (const listener of this.listeners) {
listener(event);
}
}
}3Step 3: Define the event data interface
// Usage
interface StockUpdate {
symbol: string;
price: number;
}4Step 4: Wire up and test the notification flow
const stockFeed = new EventEmitter<StockUpdate>();
const unsubscribe = stockFeed.subscribe(update => {
console.log(`${update.symbol}: $${update.price}`);
});
stockFeed.emit({ symbol: "AAPL", price: 182.5 });
stockFeed.emit({ symbol: "GOOG", price: 141.3 });
unsubscribe();
stockFeed.emit({ symbol: "AAPL", price: 183.0 }); // not received// ── Typed Event Bus with Filtering ────────────────────────────────
type EventMap = Record<string, unknown>;
type Unsubscribe = () => void;
interface SubscribeOptions<T> {
filter?: (event: T) => boolean;
once?: boolean;
}
class TypedEventBus<TEvents extends EventMap> {
private handlers = new Map<keyof TEvents, Set<{
fn: (event: unknown) => void;
filter?: (event: unknown) => boolean;
once: boolean;
}>>();
on<K extends keyof TEvents>(
event: K,
handler: (event: TEvents[K]) => void,
options: SubscribeOptions<TEvents[K]> = {}
): Unsubscribe {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
const entry = {
fn: handler as (event: unknown) => void,
filter: options.filter as ((event: unknown) => boolean) | undefined,
once: options.once ?? false,
};
this.handlers.get(event)!.add(entry);
return () => this.handlers.get(event)?.delete(entry);
}
once<K extends keyof TEvents>(
event: K,
handler: (event: TEvents[K]) => void,
options: Omit<SubscribeOptions<TEvents[K]>, "once"> = {}
): Unsubscribe {
return this.on(event, handler, { ...options, once: true });
}
emit<K extends keyof TEvents>(event: K, payload: TEvents[K]): void {
const entries = this.handlers.get(event);
if (!entries) return;
for (const entry of [...entries]) {
if (entry.filter && !entry.filter(payload)) continue;
entry.fn(payload);
if (entry.once) entries.delete(entry);
}
}
off<K extends keyof TEvents>(event: K): void {
this.handlers.delete(event);
}
}
// ── Usage ─────────────────────────────────────────────────────────
interface AppEvents extends EventMap {
"user:login": { userId: string; timestamp: number };
"user:logout": { userId: string };
"order:placed": { orderId: string; total: number; userId: string };
"order:shipped": { orderId: string; trackingCode: string };
}
const bus = new TypedEventBus<AppEvents>();
bus.on("order:placed", ({ orderId, total }) => {
console.log(`New order ${orderId} for $${total}`);
});
bus.on(
"order:placed",
({ orderId }) => console.log(`High-value order alert: ${orderId}`),
{ filter: e => e.total > 500 }
);
bus.once("user:login", ({ userId }) => {
console.log(`Welcome back, ${userId}!`);
});
export { TypedEventBus, type EventMap, type Unsubscribe };Observer Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Observer Pattern in the Real World
“Think of a newspaper subscription service. The publisher (subject) doesn't know exactly who its subscribers (observers) are—it just maintains a list. When a new edition is printed, it delivers a copy to every subscriber on the list automatically. Subscribers can cancel at any time without the publisher needing to change anything.”