Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Type-safe Event System w TypeScript

W Parku Jurajskim każdy system musi natychmiast reagować na zdarzenia — alarm o ucieczce raptorai, zmiana stanu ogrodzenia, aktualizacja pozycji dinozaura. Taki system zdarzeń (event system) wymaga precyzji — nie możesz wysłać danych o temperaturze do handlera oczekującego współrzędnych GPS. TypeScript pozwala stworzyć w pełni typowany system eventów, gdzie kompilator pilnuje poprawności za Ciebie.

EventMap — mapa typów zdarzeń

Fundamentem type-safe event systemu jest EventMap — interfejs mapujący nazwy zdarzeń na typy ich danych (payload):

1// Definiujemy mapę zdarzeń parku
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 zna dokładny kształt danych dla każdego zdarzenia
11// 'dinosaur:escaped' => { species: string; paddock: string; dangerLevel: number }
12// 'fence:status' => { zone: string; voltage: number; active: boolean }

Generic EventEmitter z TypeScript

Teraz zbudujemy generyczny emitter eventów, który wymusza zgodność typów:

1// Generyczny EventEmitter - T to mapa eventów
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// Użycie z ParkEventMap
39const parkEvents = new TypedEventEmitter<ParkEventMap>();
40
41// TypeScript zna typ parametru handler — auto-complete działa!
42parkEvents.on('dinosaur:escaped', (data) => {
43  // data jest typu { species: string; paddock: string; dangerLevel: number }
44  console.log(`ALARM! ${data.species} uciekł z ${data.paddock}!`);
45  console.log(`Poziom zagrożenia: ${data.dangerLevel}`);
46});
47
48parkEvents.on('fence:status', (data) => {
49  // data jest typu { zone: string; voltage: number; active: boolean }
50  if (!data.active) {
51    console.log(`Ogrodzenie w strefie ${data.zone} wyłączone!`);
52  }
53});
54
55// Poprawne emisje
56parkEvents.emit('dinosaur:escaped', {
57  species: 'Velociraptor',
58  paddock: 'B-7',
59  dangerLevel: 5
60});
61
62// Błąd kompilacji! Brakuje pola 'dangerLevel'
63// parkEvents.emit('dinosaur:escaped', { species: 'T-Rex', paddock: 'A-1' });
64
65// Błąd kompilacji! Nieznane zdarzenie
66// parkEvents.emit('dinosaur:sleeping', { species: 'Stegosaurus' });

Pattern: Strongly-typed addEventListener

W przeglądarkach

addEventListener
jest typowany właśnie dzięki mapie eventów. Zbudujmy własną wersję tego wzorca:

1// Mapa eventów dla komponentu UI
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// Klasa komponentu z typowanymi eventami
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// Rozszerzony komponent z własnymi eventami
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// Użycie
58const parkMap = new ParkMapComponent();
59
60parkMap.addEventListener('marker:click', (event) => {
61  // event: { markerId: string; lat: number; lng: number }
62  console.log(`Kliknięto marker ${event.markerId} na ${event.lat}, ${event.lng}`);
63});
64
65parkMap.addEventListener('click', (event) => {
66  // event: { x: number; y: number; target: string }
67  console.log(`Kliknięto na ${event.x}, ${event.y}`);
68});

Middleware pattern z typowanymi eventami

Zaawansowany wzorzec — middleware, który przetwarza eventy przed dotarciem do handlera:

1// Typ middleware — przetwarza event i decyduje czy go przepuścić
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    // Budujemy łańcuch middleware
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// Przykład: logowanie i walidacja eventów
40const securitySystem = new MiddlewareEventEmitter<ParkEventMap>();
41
42// Middleware logujący
43securitySystem.use('dinosaur:escaped', (data, next) => {
44  console.log(`[LOG] Zdarzenie ucieczki: ${data.species}`);
45  next(data);
46});
47
48// Middleware eskalujący poziom zagrożenia
49securitySystem.use('dinosaur:escaped', (data, next) => {
50  if (data.species === 'T-Rex' && data.dangerLevel < 5) {
51    next({ ...data, dangerLevel: 5 }); // Eskalacja!
52  } else {
53    next(data);
54  }
55});
56
57securitySystem.on('dinosaur:escaped', (data) => {
58  console.log(`Finalny alert: ${data.species}, zagrożenie: ${data.dangerLevel}`);
59});
60
61securitySystem.emit('dinosaur:escaped', {
62  species: 'T-Rex',
63  paddock: 'A-1',
64  dangerLevel: 3 // Zostanie eskalowany do 5 przez middleware
65});

Wyciąganie typów zdarzeń (utility types dla eventów)

1// Utility types do pracy z event mapami
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// Wyciągnięcie handlera dla konkretnego eventu
7type ParkEscapeHandler = EventHandler<ParkEventMap, 'dinosaur:escaped'>;
8// (data: { species: string; paddock: string; dangerLevel: number }) => void
9
10// Typ unii wszystkich payloadów
11type AnyParkPayload = ParkEventMap[keyof ParkEventMap];
12
13// Filtrowanie eventów po typie payloadu
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'

Type-safe event system to wzorzec, który sprawdza się wszędzie — od systemów monitoringu w Parku Jurajskim po frontend React, backendy Node.js i mikroserwisy. Generyczny

TypedEventEmitter
z
EventMap
daje pełne type safety — kompilator pilnuje, że emitujesz i nasłuchujesz dokładnie tych danych, które zadeklarowałeś.

Przejdź do CodeWorlds