"W Parku Jurajskim nie wystarczy wiedzieć, że masz przed sobą dinozaura," mówi Robert Muldoon, główny łowczy parku. "Musisz dokładnie wiedzieć, CZY to drapieżnik, CZY roślinożerca, CZY lata, CZY pływa. Od tego zależy, jaki protokół bezpieczeństwa uruchomisz. Jedno złe założenie i masz katastrofę."
Podobnie działa type narrowing w TypeScript - to proces zawężania szerokiego typu do bardziej konkretnego. Gdy zmienna ma typ
string | number, TypeScript nie wie, czy to tekst czy liczba. Dzięki type guards możemy "zawęzić" typ, by kompilator wiedział dokładnie, z czym ma do czynienia. To fundamentalna technika pisania bezpiecznego kodu TypeScript.Najprostszym type guardem jest
typeof - operator JavaScript, który TypeScript rozumie i wykorzystuje do zawężania typów. Gdy sprawdzisz typ zmiennej za pomocą typeof, kompilator automatycznie wie, jaki typ ma zmienna wewnątrz bloku if.1// Funkcja przyjmująca różne typy danych dinozaura
2function formatDinosaurData(value: string | number | boolean): string {
3 if (typeof value === "string") {
4 // TypeScript wie, że tutaj value jest typu string
5 return value.toUpperCase();
6 }
7 if (typeof value === "number") {
8 // TypeScript wie, że tutaj value jest typu number
9 return value.toFixed(2) + " kg";
10 }
11 // TypeScript wie, że tutaj value jest typu boolean
12 return value ? "Aktywny" : "Nieaktywny";
13}
14
15console.log(formatDinosaurData("Tyrannosaurus Rex")); // "TYRANNOSAURUS REX"
16console.log(formatDinosaurData(7500)); // "7500.00 kg"
17console.log(formatDinosaurData(true)); // "Aktywny"W powyższym przykładzie TypeScript automatycznie zawęża typ
value w każdym bloku if. Po sprawdzeniu typeof value === "string" kompilator wie, że wewnątrz tego bloku value jest stringiem, więc możesz bezpiecznie wywoływać .toUpperCase(). To działa z typami "string", "number", "boolean", "bigint", "symbol", "undefined", "object" i "function".Gdy pracujesz z klasami,
instanceof pozwala sprawdzić, czy obiekt jest instancją konkretnej klasy. TypeScript automatycznie zawęża typ po takim sprawdzeniu.1class Carnivore {
2 name: string;
3 biteForce: number;
4
5 constructor(name: string, biteForce: number) {
6 this.name = name;
7 this.biteForce = biteForce;
8 }
9
10 hunt(): string {
11 return `${this.name} poluje z siłą ugryzienia ${this.biteForce}N!`;
12 }
13}
14
15class Herbivore {
16 name: string;
17 favoriteFood: string;
18
19 constructor(name: string, favoriteFood: string) {
20 this.name = name;
21 this.favoriteFood = favoriteFood;
22 }
23
24 graze(): string {
25 return `${this.name} spokojnie zjada ${this.favoriteFood}`;
26 }
27}
28
29// Funkcja obsługująca oba typy dinozaurów
30function handleDinosaur(dino: Carnivore | Herbivore): string {
31 if (dino instanceof Carnivore) {
32 // TypeScript wie, że tutaj dino jest Carnivore
33 return dino.hunt(); // Mamy dostęp do hunt() i biteForce
34 }
35 // TypeScript wie, że tutaj dino jest Herbivore
36 return dino.graze(); // Mamy dostęp do graze() i favoriteFood
37}
38
39const rex = new Carnivore("T-Rex", 12800);
40const trike = new Herbivore("Triceratops", "Paprocie");
41
42console.log(handleDinosaur(rex)); // "T-Rex poluje z siłą ugryzienia 12800N!"
43console.log(handleDinosaur(trike)); // "Triceratops spokojnie zjada Paprocie"Operator
in sprawdza, czy obiekt posiada daną właściwość. TypeScript wykorzystuje to do zawężania typów w union types.1interface FlyingDinosaur {
2 name: string;
3 maxAltitude: number;
4 wingspan: number;
5}
6
7interface SwimmingDinosaur {
8 name: string;
9 maxDepth: number;
10 swimSpeed: number;
11}
12
13interface LandDinosaur {
14 name: string;
15 runSpeed: number;
16 weight: number;
17}
18
19type Dinosaur = FlyingDinosaur | SwimmingDinosaur | LandDinosaur;
20
21function describeDinosaur(dino: Dinosaur): string {
22 if ("maxAltitude" in dino) {
23 // TypeScript wie, że to FlyingDinosaur
24 return `${dino.name} lata na wysokości do ${dino.maxAltitude}m, rozpiętość skrzydeł: ${dino.wingspan}m`;
25 }
26 if ("maxDepth" in dino) {
27 // TypeScript wie, że to SwimmingDinosaur
28 return `${dino.name} nurkuje do ${dino.maxDepth}m, prędkość: ${dino.swimSpeed} km/h`;
29 }
30 // TypeScript wie, że to LandDinosaur
31 return `${dino.name} biega z prędkością ${dino.runSpeed} km/h, waży ${dino.weight} kg`;
32}
33
34const pteranodon: FlyingDinosaur = { name: "Pteranodon", maxAltitude: 500, wingspan: 7 };
35const mosasaurus: SwimmingDinosaur = { name: "Mosasaurus", maxDepth: 200, swimSpeed: 48 };
36const velociraptor: LandDinosaur = { name: "Velociraptor", runSpeed: 64, weight: 15 };
37
38console.log(describeDinosaur(pteranodon));
39console.log(describeDinosaur(mosasaurus));
40console.log(describeDinosaur(velociraptor));Operator
in jest szczególnie przydatny, gdy pracujesz z interfejsami (nie klasami), bo instanceof działa tylko z klasami, natomiast in sprawdza po prostu obecność właściwości.Możesz tworzyć własne funkcje typu guard, które zwracają specjalny typ
parameterName is Type. Dzięki temu TypeScript rozumie, że po pozytywnym sprawdzeniu zmienna ma konkretny typ.1interface DinoEgg {
2 species: string;
3 weight: number;
4 incubationDays: number;
5}
6
7interface HatchedDino {
8 species: string;
9 weight: number;
10 age: number;
11 enclosureId: string;
12}
13
14// Custom type guard - funkcja zwraca "specimen is HatchedDino"
15function isHatched(specimen: DinoEgg | HatchedDino): specimen is HatchedDino {
16 return "age" in specimen && "enclosureId" in specimen;
17}
18
19function processSpecimen(specimen: DinoEgg | HatchedDino): string {
20 if (isHatched(specimen)) {
21 // TypeScript wie, że tutaj specimen jest HatchedDino
22 return `${specimen.species} - wiek: ${specimen.age} dni, zagroda: ${specimen.enclosureId}`;
23 }
24 // TypeScript wie, że tutaj specimen jest DinoEgg
25 return `Jajo ${specimen.species} - inkubacja: ${specimen.incubationDays} dni`;
26}
27
28// Bardziej zaawansowany type guard z walidacją
29function isCarnivoreData(data: unknown): data is { species: string; biteForce: number; diet: "carnivore" } {
30 return (
31 typeof data === "object" &&
32 data !== null &&
33 "species" in data &&
34 "biteForce" in data &&
35 "diet" in data &&
36 (data as any).diet === "carnivore"
37 );
38}
39
40// Użycie z nieznanym źródłem danych (np. API)
41function processParkData(rawData: unknown): string {
42 if (isCarnivoreData(rawData)) {
43 // TypeScript wie, że rawData ma pola species, biteForce, diet
44 return `Drapieżnik: ${rawData.species}, siła: ${rawData.biteForce}N`;
45 }
46 return "Nieznany typ danych";
47}
48
49console.log(processParkData({ species: "T-Rex", biteForce: 12800, diet: "carnivore" }));
50console.log(processParkData({ name: "Triceratops" }));Custom type guards są szczególnie przydatne, gdy:
unknownDiscriminated unions to wzorzec, w którym każdy typ w unii ma wspólne pole "dyskryminujące" (tag), które jednoznacznie identyfikuje typ. TypeScript doskonale radzi sobie z zawężaniem typów na podstawie tego pola.
1// Każdy typ ma pole "kind" jako dyskryminator
2interface SecurityAlert {
3 kind: "security";
4 zone: string;
5 threatLevel: number;
6 dinosaurId: string;
7}
8
9interface MedicalAlert {
10 kind: "medical";
11 dinosaurId: string;
12 symptoms: string[];
13 veterinarianRequired: boolean;
14}
15
16interface SystemAlert {
17 kind: "system";
18 component: string;
19 errorCode: number;
20 message: string;
21}
22
23type ParkAlert = SecurityAlert | MedicalAlert | SystemAlert;
24
25function handleAlert(alert: ParkAlert): string {
26 switch (alert.kind) {
27 case "security":
28 // TypeScript wie, że to SecurityAlert
29 return `ALARM BEZPIECZEŃSTWA! Strefa: ${alert.zone}, Zagrożenie: ${alert.threatLevel}/10, Dinozaur: ${alert.dinosaurId}`;
30 case "medical":
31 // TypeScript wie, że to MedicalAlert
32 return `ALERT MEDYCZNY! Dinozaur: ${alert.dinosaurId}, Objawy: ${alert.symptoms.join(", ")}`;
33 case "system":
34 // TypeScript wie, że to SystemAlert
35 return `AWARIA SYSTEMU! Komponent: ${alert.component}, Kod: ${alert.errorCode}`;
36 }
37}Typ
never w TypeScript oznacza "wartość, która nigdy nie wystąpi". Możemy go użyć do sprawdzenia, czy obsłużyliśmy WSZYSTKIE warianty discriminated union. Jeśli dodamy nowy wariant i zapomnimy go obsłużyć, TypeScript zgłosi błąd kompilacji.1// Exhaustive check - TypeScript ostrzeże nas, jeśli pominiemy jakikolwiek wariant
2function handleAlertExhaustive(alert: ParkAlert): string {
3 switch (alert.kind) {
4 case "security":
5 return `Bezpieczeństwo: ${alert.zone}`;
6 case "medical":
7 return `Medyczny: ${alert.dinosaurId}`;
8 case "system":
9 return `System: ${alert.component}`;
10 default:
11 // Jeśli dodamy nowy typ alertu i zapomnimy go obsłużyć,
12 // TypeScript zgłosi błąd, bo alert nie będzie typu never
13 const exhaustiveCheck: never = alert;
14 return exhaustiveCheck;
15 }
16}
17
18// Praktyczny helper do exhaustive check
19function assertNever(value: never, message: string = "Nieobsłużony wariant"): never {
20 throw new Error(`${message}: ${JSON.stringify(value)}`);
21}Exhaustive check to potężna technika - gdy dodasz np.
WeatherAlert do unii ParkAlert, kompilator NATYCHMIAST wskaże wszystkie miejsca w kodzie, gdzie brakuje obsługi nowego wariantu. To jak system wczesnego ostrzegania w Parku Jurajskim - nie pozwala na przeoczenie żadnego zagrożenia.Poniżej bardziej rozbudowany przykład łączący różne techniki type narrowing w jednym systemie:
1// System klasyfikacji próbek laboratoryjnych
2type SampleStatus = "collected" | "analyzing" | "completed" | "contaminated";
3
4interface BaseSample {
5 id: string;
6 collectedAt: Date;
7 status: SampleStatus;
8}
9
10interface DNASample extends BaseSample {
11 type: "dna";
12 species: string;
13 purity: number;
14 sequenceLength: number;
15}
16
17interface BloodSample extends BaseSample {
18 type: "blood";
19 species: string;
20 volumeMl: number;
21 pH: number;
22}
23
24interface FossilSample extends BaseSample {
25 type: "fossil";
26 era: string;
27 estimatedAge: number;
28}
29
30type LabSample = DNASample | BloodSample | FossilSample;
31
32// Custom type guard
33function isDNASample(sample: LabSample): sample is DNASample {
34 return sample.type === "dna";
35}
36
37// Sprawdzenie statusu za pomocą typeof i custom guard
38function canProcess(sample: LabSample): boolean {
39 return sample.status === "collected" || sample.status === "analyzing";
40}
41
42// Główna funkcja przetwarzania - łączy różne techniki
43function processLabSample(sample: LabSample): string {
44 // Discriminated union narrowing
45 switch (sample.type) {
46 case "dna":
47 if (sample.purity < 50) {
48 return `Próbka DNA ${sample.id}: zbyt niska czystość (${sample.purity}%)`;
49 }
50 return `Analiza DNA ${sample.species}: ${sample.sequenceLength} par zasad, czystość ${sample.purity}%`;
51
52 case "blood":
53 const phStatus = sample.pH >= 7.0 && sample.pH <= 7.6 ? "norma" : "POZA NORMĄ";
54 return `Analiza krwi ${sample.species}: ${sample.volumeMl}ml, pH ${sample.pH} (${phStatus})`;
55
56 case "fossil":
57 return `Skamieniałość z ery ${sample.era}: szacowany wiek ${sample.estimatedAge} mln lat`;
58
59 default:
60 const _exhaustive: never = sample;
61 return _exhaustive;
62 }
63}
64
65// Test
66const dnaSample: DNASample = {
67 id: "DNA-042", type: "dna", collectedAt: new Date(),
68 status: "collected", species: "Velociraptor", purity: 87, sequenceLength: 75000000
69};
70
71const bloodSample: BloodSample = {
72 id: "BLD-015", type: "blood", collectedAt: new Date(),
73 status: "analyzing", species: "Triceratops", volumeMl: 120, pH: 7.2
74};
75
76const fossilSample: FossilSample = {
77 id: "FOS-003", type: "fossil", collectedAt: new Date(),
78 status: "completed", era: "Kreda", estimatedAge: 68
79};
80
81console.log(processLabSample(dnaSample));
82console.log(processLabSample(bloodSample));
83console.log(processLabSample(fossilSample));Type narrowing i type guards to fundamentalne narzędzia TypeScript, które pozwalają pisać bezpieczny kod przy jednoczesnym zachowaniu elastyczności union types:
string, number, boolean)Jak mówi Robert Muldoon: "Identyfikacja gatunku to pierwszy krok protokołu bezpieczeństwa. Podobnie type narrowing to pierwszy krok do bezpiecznego kodu - nigdy nie zakładaj, z czym masz do czynienia. Sprawdź, zawęź typ i działaj pewnie."