In Jurassic Park, every system must immediately react to events — an alarm about a raptor escape, a fence status change, a dinosaur position update. Such an event system requires precision — you cannot send temperature data to a handler expecting GPS coordinates. TypeScript allows you to create a fully typed event system where the compiler ensures correctness for you.
The foundation of a type-safe event system is an EventMap — an interface mapping event names to their data types (payload):
1// Defining the park event map
2interface ParkEventMap {
3 'dinosaur:escaped': { species: string; paddock: string; dangerLevel: number };
4 'fence:status': { zone: string; voltage: number; active: boolean };
5 'visitor:enter': { visitorId: string; gate: string; timestamp: Date };
6 'visitor:exit': { visitorId: string; gate: string; timestamp: Date };
7 'alert:weather': { type: 'storm' | 'heat' | 'flood'; severity: 1 | 2 | 3 };
8}
9
10// TypeScript knows the exact shape of data for each event
11// 'dinosaur:escaped' => { species: string; paddock: string; dangerLevel: number }
12// 'fence:status' => { zone: string; voltage: number; active: boolean }Now let's build a generic event emitter that enforces type consistency:
1// Generic EventEmitter - T is the event map
2class TypedEventEmitter<T extends Record<string, any>> {
3 private listeners: {
4 [K in keyof T]?: Array<(data: T[K]) => void>;
5 } = {};
6
7 on<K extends keyof T>(event: K, handler: (data: T[K]) => void): void {
8 if (!this.listeners[event]) {
9 this.listeners[event] = [];
10 }
11 this.listeners[event]!.push(handler);
12 }
13
14 off<K extends keyof T>(event: K, handler: (data: T[K]) => void): void {
15 const handlers = this.listeners[event];
16 if (handlers) {
17 const index = handlers.indexOf(handler);
18 if (index > -1) handlers.splice(index, 1);
19 }
20 }
21
22 emit<K extends keyof T>(event: K, data: T[K]): void {
23 const handlers = this.listeners[event];
24 if (handlers) {
25 handlers.forEach(handler => handler(data));
26 }
27 }
28
29 once<K extends keyof T>(event: K, handler: (data: T[K]) => void): void {
30 const wrapper = (data: T[K]) => {
31 handler(data);
32 this.off(event, wrapper);
33 };
34 this.on(event, wrapper);
35 }
36}
37
38// Usage with ParkEventMap
39const parkEvents = new TypedEventEmitter<ParkEventMap>();
40
41// TypeScript knows the handler parameter type — auto-complete works!
42parkEvents.on('dinosaur:escaped', (data) => {
43 // data is of type { species: string; paddock: string; dangerLevel: number }
44 console.log(`ALARM! ${data.species} escaped from ${data.paddock}!`);
45 console.log(`Danger level: ${data.dangerLevel}`);
46});
47
48parkEvents.on('fence:status', (data) => {
49 // data is of type { zone: string; voltage: number; active: boolean }
50 if (!data.active) {
51 console.log(`Fence in zone ${data.zone} deactivated!`);
52 }
53});
54
55// Correct emissions
56parkEvents.emit('dinosaur:escaped', {
57 species: 'Velociraptor',
58 paddock: 'B-7',
59 dangerLevel: 5
60});
61
62// Compilation error! Missing field 'dangerLevel'
63// parkEvents.emit('dinosaur:escaped', { species: 'T-Rex', paddock: 'A-1' });
64
65// Compilation error! Unknown event
66// parkEvents.emit('dinosaur:sleeping', { species: 'Stegosaurus' });In browsers,
addEventListener is typed precisely thanks to event maps. Let's build our own version of this pattern:1// Event map for a UI component
2interface UIEventMap {
3 click: { x: number; y: number; target: string };
4 hover: { x: number; y: number; element: string };
5 keypress: { key: string; code: number; ctrl: boolean; shift: boolean };
6 resize: { width: number; height: number };
7 scroll: { scrollTop: number; scrollLeft: number };
8}
9
10// Component class with typed events
11class UIComponent<Events extends Record<string, any> = UIEventMap> {
12 private eventHandlers: Map<keyof Events, Set<Function>> = new Map();
13
14 addEventListener<K extends keyof Events>(
15 type: K,
16 listener: (event: Events[K]) => void
17 ): void {
18 if (!this.eventHandlers.has(type)) {
19 this.eventHandlers.set(type, new Set());
20 }
21 this.eventHandlers.get(type)!.add(listener);
22 }
23
24 removeEventListener<K extends keyof Events>(
25 type: K,
26 listener: (event: Events[K]) => void
27 ): void {
28 this.eventHandlers.get(type)?.delete(listener);
29 }
30
31 protected dispatchEvent<K extends keyof Events>(
32 type: K,
33 event: Events[K]
34 ): void {
35 this.eventHandlers.get(type)?.forEach(handler => {
36 (handler as (event: Events[K]) => void)(event);
37 });
38 }
39}
40
41// Extended component with custom events
42interface MapComponentEvents extends UIEventMap {
43 'marker:click': { markerId: string; lat: number; lng: number };
44 'zone:select': { zoneId: string; name: string };
45}
46
47class ParkMapComponent extends UIComponent<MapComponentEvents> {
48 selectZone(zoneId: string, name: string): void {
49 this.dispatchEvent('zone:select', { zoneId, name });
50 }
51
52 clickMarker(markerId: string, lat: number, lng: number): void {
53 this.dispatchEvent('marker:click', { markerId, lat, lng });
54 }
55}
56
57// Usage
58const parkMap = new ParkMapComponent();
59
60parkMap.addEventListener('marker:click', (event) => {
61 // event: { markerId: string; lat: number; lng: number }
62 console.log(`Clicked marker ${event.markerId} at ${event.lat}, ${event.lng}`);
63});
64
65parkMap.addEventListener('click', (event) => {
66 // event: { x: number; y: number; target: string }
67 console.log(`Clicked at ${event.x}, ${event.y}`);
68});An advanced pattern — middleware that processes events before they reach the handler:
1// Middleware type — processes event and decides whether to pass it through
2type EventMiddleware<T> = (
3 data: T,
4 next: (data: T) => void
5) => void;
6
7class MiddlewareEventEmitter<T extends Record<string, any>> {
8 private listeners: { [K in keyof T]?: Array<(data: T[K]) => void> } = {};
9 private middleware: { [K in keyof T]?: Array<EventMiddleware<T[K]>> } = {};
10
11 on<K extends keyof T>(event: K, handler: (data: T[K]) => void): void {
12 if (!this.listeners[event]) this.listeners[event] = [];
13 this.listeners[event]!.push(handler);
14 }
15
16 use<K extends keyof T>(event: K, mw: EventMiddleware<T[K]>): void {
17 if (!this.middleware[event]) this.middleware[event] = [];
18 this.middleware[event]!.push(mw);
19 }
20
21 emit<K extends keyof T>(event: K, data: T[K]): void {
22 const mws = this.middleware[event] || [];
23 const handlers = this.listeners[event] || [];
24
25 // Building the middleware chain
26 const chain = mws.reduceRight(
27 (next: (data: T[K]) => void, mw: EventMiddleware<T[K]>) => {
28 return (d: T[K]) => mw(d, next);
29 },
30 (finalData: T[K]) => {
31 handlers.forEach(h => h(finalData));
32 }
33 );
34
35 chain(data);
36 }
37}
38
39// Example: logging and validating events
40const securitySystem = new MiddlewareEventEmitter<ParkEventMap>();
41
42// Logging middleware
43securitySystem.use('dinosaur:escaped', (data, next) => {
44 console.log(`[LOG] Escape event: ${data.species}`);
45 next(data);
46});
47
48// Danger level escalation middleware
49securitySystem.use('dinosaur:escaped', (data, next) => {
50 if (data.species === 'T-Rex' && data.dangerLevel < 5) {
51 next({ ...data, dangerLevel: 5 }); // Escalation!
52 } else {
53 next(data);
54 }
55});
56
57securitySystem.on('dinosaur:escaped', (data) => {
58 console.log(`Final alert: ${data.species}, danger: ${data.dangerLevel}`);
59});
60
61securitySystem.emit('dinosaur:escaped', {
62 species: 'T-Rex',
63 paddock: 'A-1',
64 dangerLevel: 3 // Will be escalated to 5 by middleware
65});1// Utility types for working with event maps
2type EventNames<T> = keyof T & string;
3type EventPayload<T, K extends keyof T> = T[K];
4type EventHandler<T, K extends keyof T> = (data: T[K]) => void;
5
6// Extracting handler for a specific event
7type ParkEscapeHandler = EventHandler<ParkEventMap, 'dinosaur:escaped'>;
8// (data: { species: string; paddock: string; dangerLevel: number }) => void
9
10// Union type of all payloads
11type AnyParkPayload = ParkEventMap[keyof ParkEventMap];
12
13// Filtering events by payload type
14type EventsWithField<T, Field extends string> = {
15 [K in keyof T]: Field extends keyof T[K] ? K : never;
16}[keyof T];
17
18type EventsWithVisitorId = EventsWithField<ParkEventMap, 'visitorId'>;
19// 'visitor:enter' | 'visitor:exit'A type-safe event system is a pattern that works everywhere — from monitoring systems in Jurassic Park to React frontends, Node.js backends, and microservices. A generic
TypedEventEmitter with an EventMap provides full type safety — the compiler ensures that you emit and listen for exactly the data you declared.