Event Bus and Observer Pattern in TypeScript: From Theory to Practice
Modern web development requires a flexible and scalable architecture. Two key patterns that help organize interaction between components are Event Bus and Observer. In this article, we will break down their implementation in TypeScript, advantages, and practical use cases.
What is the Observer Pattern?
The Observer pattern is a behavioral design pattern that defines a one-to-many dependency between objects. When one object (the publisher) changes its state, all dependent objects (subscribers) are automatically notified and updated.
Main elements of the pattern:
- Subject (Publisher) — the object that tracks changes and notifies subscribers.
- Observer (Subscriber) — the object that subscribes to changes from the publisher.
- Event Bus — a centralized mechanism that manages subscriptions and dispatches events between components.
Implementing Event Bus in TypeScript
TypeScript provides excellent capabilities for strict typing, making the Event Bus implementation safe and predictable. Let's look at a basic implementation:
type EventHandler<T = any> = (payload: T) => void;
class EventBus { private listeners: Map<string, EventHandler[]> = new Map();
// Subscribe to an event subscribe<T>(event: string, handler: EventHandler<T>): void { if (!this.listeners.has(event)) { this.listeners.set(event, []); } this.listeners.get(event)!.push(handler); }
// Unsubscribe from an event unsubscribe<T>(event: string, handler: EventHandler<T>): void { const handlers = this.listeners.get(event); if (handlers) { this.listeners.set( event, handlers.filter(h => h !== handler) ); } }
// Publish an event emit<T>(event: string, payload: T): void { const handlers = this.listeners.get(event); if (handlers) { handlers.forEach(handler => handler(payload)); } }
// Clear all subscriptions clear(): void { this.listeners.clear(); }}
export const eventBus = new EventBus();Usage Example with Typed Events
To improve type safety, you can define event interfaces:
// Define event typesinterface AppEvents { 'user:login': { userId: string; timestamp: number }; 'user:logout': { userId: string }; 'data:update': { id: string; data: unknown }; 'notification:show': { message: string; type: 'success' | 'error' | 'info' };}
// Create a typed busclass TypedEventBus { private listeners: Map<string, Function[]> = new Map();
on<K extends keyof AppEvents>( event: K, handler: (payload: AppEvents[K]) => void ): void { if (!this.listeners.has(event)) { this.listeners.set(event, []); } this.listeners.get(event)!.push(handler); }
emit<K extends keyof AppEvents>( event: K, payload: AppEvents[K] ): void { const handlers = this.listeners.get(event); if (handlers) { handlers.forEach(handler => handler(payload)); } }
off<K extends keyof AppEvents>( event: K, handler: (payload: AppEvents[K]) => void ): void { const handlers = this.listeners.get(event); if (handlers) { this.listeners.set( event, handlers.filter(h => h !== handler) ); } }}
export const typedBus = new TypedEventBus();Practical Example: Notification System
Let's consider a real-world scenario — creating a notification system using Event Bus: