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.
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 }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' });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});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});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ś.