Event-driven architecture in TypeScript: when code "hears" each other
\n\nImagine you are writing a complex web application: a messenger, a trading bot, or a server monitoring system. At some point, you realize: simply calling functions in a chain is a nightmare. One module pulls another, a third depends on a fourth, and when the logic changes, everything falls apart like a house of cards.
\n\nSound familiar? Then you need event-driven architecture in TypeScript. This is not just a trendy term, but a real way to make code flexible, extensible, and understandable. In this article, we will break down how event-driven programming works, write our own event bus in TypeScript, and see where it really helps.
\n\nWhat is Event-driven architecture in simple terms
\n\nEvent-driven architecture is an approach where system components communicate through events. One module generates an event (for example, "user registered"), and others subscribe to it and react. They are not directly connected to each other.
\n\nSounds abstract? Let's use a real-life example. Imagine an office:
\n- \n
- Traditional approach: The boss personally approaches each employee and says: "Make a report." \n
- Event-driven: The boss shouts: "A report is needed!" — and everyone who knows how to make it starts working on it. \n
In the second case, the boss doesn't need to know who exactly will make the report. He simply sends an event. And employees can connect and disconnect without changing the boss's logic. This is the essence of event-driven programming.
\n\nWhy TypeScript is ideal for event-driven architecture
\n\nJavaScript itself is event-driven (remember addEventListener), but in large projects without types, chaos begins. TypeScript events solve this problem: you know exactly what data comes in the event, and the compiler will catch the error at development time, not at runtime.
Additionally, TypeScript supports generics, overloads, and strict typing of callbacks — everything needed to create a reliable event bus.
\n\nWriting an Event Bus in TypeScript from scratch
\n\nLet's start with a simple but working example. Let's create a class EventBus — a central event bus through which all modules will communicate.
// event-bus.ts\ntype EventHandler<T = any> = (data: T) => void;\n\nclass EventBus {\n private handlers: Map<string, EventHandler[]> = new Map();\n\n // Subscribe to an event\n on<T>(event: string, handler: EventHandler<T>): void {\n if (!this.handlers.has(event)) {\n this.handlers.set(event, []);\n }\n this.handlers.get(event)!.push(handler as EventHandler);\n }\n\n // Unsubscribe from an event\n off(event: string, handler: EventHandler): void {\n const handlers = this.handlers.get(event);\n if (handlers) {\n this.handlers.set(event, handlers.filter(h => h !== handler));\n }\n }\n\n // Emit an event\n emit<T>(event: string, data: T): void {\n const handlers = this.handlers.get(event);\n if (handlers) {\n handlers.forEach(handler => handler(data));\n }\n }\n}\n\nexport const globalEventBus = new EventBus();\n\nAnd there we have it — we've written a simple event-driven architecture in TypeScript. Now any module can import globalEventBus and either listen to events or generate them.
Usage example: notifications and logging
\n\nLet's imagine we have an authentication module and a notification module. They should not know about each other directly.
\n\n// auth-module.ts\nimport { globalEventBus } from './event-bus';\n\nexport function loginUser(email: string): void {\n // ... login logic\n console.log(`User