"W centrum kontroli Parku Jurajskiego każda sytuacja wymaga innej reakcji," wyjaśnia Claire Dearing, dyrektor operacyjna parku. "Ucieczka dinozaura, awaria zasilania, kontuzja gościa - każdy incydent ma swój protokół. Nie możemy pomylić jednego z drugim, bo konsekwencje byłyby katastrofalne. System musi sam rozpoznawać typ zdarzenia i kierować nas do właściwej procedury."
Discriminated unions (unie dyskryminowane) to potężny wzorzec TypeScript, w którym każdy wariant unii posiada wspólne pole - dyskryminator (tag) - jednoznacznie identyfikujące typ. W połączeniu z pattern matchingiem opartym o
switch tworzą elegancki, bezpieczny i rozszerzalny system obsługi różnych stanów i zdarzeń.Discriminated union to unia typów, w której każdy typ ma pole o tej samej nazwie (tag), ale z inną wartością literalną. TypeScript potrafi zawęzić typ na podstawie wartości tego pola.
1// Pole "status" jest dyskryminatorem
2interface ActiveDinosaur {
3 status: "active";
4 name: string;
5 enclosureId: string;
6 lastFed: Date;
7}
8
9interface SleepingDinosaur {
10 status: "sleeping";
11 name: string;
12 enclosureId: string;
13 sleepStarted: Date;
14 estimatedWakeUp: Date;
15}
16
17interface SickDinosaur {
18 status: "sick";
19 name: string;
20 enclosureId: string;
21 diagnosis: string;
22 veterinarianId: string;
23}
24
25type DinosaurState = ActiveDinosaur | SleepingDinosaur | SickDinosaur;
26
27// TypeScript zawęża typ po sprawdzeniu pola status
28function getDinosaurReport(dino: DinosaurState): string {
29 switch (dino.status) {
30 case "active":
31 // TypeScript wie, że to ActiveDinosaur - mamy dostęp do lastFed
32 return `${dino.name}: Aktywny, ostatnie karmienie: ${dino.lastFed.toLocaleString()}`;
33 case "sleeping":
34 // TypeScript wie, że to SleepingDinosaur - mamy dostęp do estimatedWakeUp
35 return `${dino.name}: Śpi, przewidywane przebudzenie: ${dino.estimatedWakeUp.toLocaleString()}`;
36 case "sick":
37 // TypeScript wie, że to SickDinosaur - mamy dostęp do diagnosis
38 return `${dino.name}: Chory - ${dino.diagnosis}, weterynarz: ${dino.veterinarianId}`;
39 }
40}Kluczowe aspekty discriminated unions:
"active", nie string)Konstrukcja
switch to naturalne narzędzie do pattern matchingu w TypeScript. Każdy case odpowiada jednemu wariantowi discriminated union.1// System zdarzeń Parku Jurajskiego
2interface FeedingEvent {
3 type: "feeding";
4 dinosaurId: string;
5 food: string;
6 amountKg: number;
7 timestamp: Date;
8}
9
10interface EscapeEvent {
11 type: "escape";
12 dinosaurId: string;
13 enclosureId: string;
14 breachType: "fence" | "gate" | "underground";
15 timestamp: Date;
16}
17
18interface MaintenanceEvent {
19 type: "maintenance";
20 systemId: string;
21 description: string;
22 priority: "low" | "medium" | "high" | "critical";
23 timestamp: Date;
24}
25
26interface VisitorEvent {
27 type: "visitor";
28 visitorCount: number;
29 zone: string;
30 eventKind: "entry" | "exit" | "emergency";
31 timestamp: Date;
32}
33
34type ParkEvent = FeedingEvent | EscapeEvent | MaintenanceEvent | VisitorEvent;
35
36// Pattern matching - obsługa każdego typu zdarzenia
37function processParkEvent(event: ParkEvent): { action: string; urgency: number } {
38 switch (event.type) {
39 case "feeding":
40 return {
41 action: `Karmienie ${event.dinosaurId}: ${event.amountKg}kg ${event.food}`,
42 urgency: 1
43 };
44
45 case "escape":
46 return {
47 action: `ALARM! ${event.dinosaurId} uciekł z ${event.enclosureId} (typ: ${event.breachType})`,
48 urgency: 10
49 };
50
51 case "maintenance":
52 const urgencyMap = { low: 2, medium: 4, high: 7, critical: 9 };
53 return {
54 action: `Konserwacja ${event.systemId}: ${event.description}`,
55 urgency: urgencyMap[event.priority]
56 };
57
58 case "visitor":
59 const visitorUrgency = event.eventKind === "emergency" ? 8 : 1;
60 return {
61 action: `Goście (${event.visitorCount}) w ${event.zone}: ${event.eventKind}`,
62 urgency: visitorUrgency
63 };
64 }
65}Jedną z największych zalet discriminated unions jest możliwość exhaustive handling - kompilator może sprawdzić, czy obsłużyłeś WSZYSTKIE warianty. Jeśli dodasz nowy typ do unii, TypeScript natychmiast wskaże wszystkie miejsca, które wymagają aktualizacji.
1// Helper do exhaustive check
2function assertNever(value: never, message?: string): never {
3 throw new Error(message || `Nieobsłużony wariant: ${JSON.stringify(value)}`);
4}
5
6// Jeśli dodamy nowy typ zdarzenia do ParkEvent i zapomnimy go obsłużyć,
7// TypeScript zgłosi błąd w klauzuli default
8function processEventStrict(event: ParkEvent): string {
9 switch (event.type) {
10 case "feeding":
11 return "Karmienie";
12 case "escape":
13 return "Ucieczka";
14 case "maintenance":
15 return "Konserwacja";
16 case "visitor":
17 return "Goście";
18 default:
19 // Gdyby brakowało jednego case, TypeScript zgłosi błąd:
20 // "Type 'XXX' is not assignable to type 'never'"
21 return assertNever(event);
22 }
23}Discriminated unions doskonale nadają się do modelowania maszyn stanów (state machines), gdzie obiekt może znajdować się w jednym z kilku stanów, a przejścia między stanami są ściśle kontrolowane.
1// Stany zagrody dinozaurów jako maszyna stanów
2interface EnclosureNormal {
3 state: "normal";
4 dinosaurCount: number;
5 securityLevel: number;
6}
7
8interface EnclosureLockdown {
9 state: "lockdown";
10 dinosaurCount: number;
11 securityLevel: number;
12 reason: string;
13 lockdownSince: Date;
14}
15
16interface EnclosureBreach {
17 state: "breach";
18 dinosaurCount: number;
19 escapedCount: number;
20 breachLocation: string;
21 responseTeamDispatched: boolean;
22}
23
24interface EnclosureMaintenance {
25 state: "maintenance";
26 dinosaurCount: 0; // Zagroda musi być pusta podczas konserwacji
27 scheduledEnd: Date;
28 maintenanceType: string;
29}
30
31type EnclosureState = EnclosureNormal | EnclosureLockdown | EnclosureBreach | EnclosureMaintenance;
32
33// Dozwolone przejścia między stanami
34function transitionEnclosure(
35 current: EnclosureState,
36 action: "lockdown" | "breach" | "repair" | "resolve"
37): EnclosureState {
38 switch (current.state) {
39 case "normal":
40 if (action === "lockdown") {
41 return {
42 state: "lockdown",
43 dinosaurCount: current.dinosaurCount,
44 securityLevel: current.securityLevel + 5,
45 reason: "Procedura bezpieczeństwa",
46 lockdownSince: new Date()
47 };
48 }
49 if (action === "repair") {
50 if (current.dinosaurCount > 0) {
51 throw new Error("Nie można rozpocząć konserwacji - zagroda zajęta!");
52 }
53 return {
54 state: "maintenance",
55 dinosaurCount: 0,
56 scheduledEnd: new Date(Date.now() + 86400000),
57 maintenanceType: "Standardowy przegląd"
58 };
59 }
60 return current;
61
62 case "lockdown":
63 if (action === "breach") {
64 return {
65 state: "breach",
66 dinosaurCount: current.dinosaurCount - 1,
67 escapedCount: 1,
68 breachLocation: "Sektor nieznany",
69 responseTeamDispatched: false
70 };
71 }
72 if (action === "resolve") {
73 return {
74 state: "normal",
75 dinosaurCount: current.dinosaurCount,
76 securityLevel: current.securityLevel
77 };
78 }
79 return current;
80
81 case "breach":
82 if (action === "resolve") {
83 return {
84 state: "lockdown",
85 dinosaurCount: current.dinosaurCount + current.escapedCount,
86 securityLevel: 10,
87 reason: "Post-breach lockdown",
88 lockdownSince: new Date()
89 };
90 }
91 return current;
92
93 case "maintenance":
94 if (action === "resolve") {
95 return {
96 state: "normal",
97 dinosaurCount: 0,
98 securityLevel: 5
99 };
100 }
101 return current;
102 }
103}
104
105// Test maszyny stanów
106let enclosure: EnclosureState = {
107 state: "normal",
108 dinosaurCount: 3,
109 securityLevel: 5
110};
111
112console.log("Stan początkowy:", enclosure.state);
113enclosure = transitionEnclosure(enclosure, "lockdown");
114console.log("Po lockdown:", enclosure.state);
115enclosure = transitionEnclosure(enclosure, "breach");
116console.log("Po breach:", enclosure.state);
117if (enclosure.state === "breach") {
118 console.log("Uciekło dinozaurów:", enclosure.escapedCount);
119}Częstym zastosowaniem w prawdziwych aplikacjach jest modelowanie odpowiedzi API za pomocą discriminated unions:
1// Generyczny typ odpowiedzi API
2interface SuccessResponse<T> {
3 status: "success";
4 data: T;
5 timestamp: Date;
6}
7
8interface ErrorResponse {
9 status: "error";
10 errorCode: number;
11 message: string;
12 timestamp: Date;
13}
14
15interface LoadingResponse {
16 status: "loading";
17 progress: number; // 0-100
18}
19
20type ApiResponse<T> = SuccessResponse<T> | ErrorResponse | LoadingResponse;
21
22// Typ danych dinozaura z API
23interface DinosaurApiData {
24 id: string;
25 name: string;
26 species: string;
27 weight: number;
28}
29
30// Obsługa odpowiedzi z pełnym bezpieczeństwem typów
31function renderDinosaurData(response: ApiResponse<DinosaurApiData>): string {
32 switch (response.status) {
33 case "loading":
34 return `Ładowanie... ${response.progress}%`;
35 case "error":
36 return `Błąd ${response.errorCode}: ${response.message}`;
37 case "success":
38 const { name, species, weight } = response.data;
39 return `${name} (${species}) - ${weight} kg`;
40 }
41}
42
43// Testowe odpowiedzi
44const loading: ApiResponse<DinosaurApiData> = { status: "loading", progress: 45 };
45const success: ApiResponse<DinosaurApiData> = {
46 status: "success",
47 data: { id: "D-001", name: "Rexy", species: "T-Rex", weight: 8000 },
48 timestamp: new Date()
49};
50const error: ApiResponse<DinosaurApiData> = {
51 status: "error",
52 errorCode: 404,
53 message: "Dinozaur nie znaleziony",
54 timestamp: new Date()
55};
56
57console.log(renderDinosaurData(loading));
58console.log(renderDinosaurData(success));
59console.log(renderDinosaurData(error));Discriminated unions i pattern matching to jedne z najpotężniejszych wzorców w TypeScript:
Jak mówi Claire Dearing: "W Parku Jurajskim każdy scenariusz ma swój protokół. Z discriminated unions TypeScript pilnuje, że żaden scenariusz nie zostanie pominięty. To jak system wczesnego ostrzegania - nie pozwala na przeoczenie żadnego zagrożenia."