Kurs JavaScript i TypeScript · Moduł 7: Zaawansowany TypeScript

Type narrowing i type guards

9 min czytania
W tej lekcji7

"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.

Operator typeof jako type guard

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".

Operator instanceof

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

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.

Custom type guard functions (słowo kluczowe is)

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:

  1. Logika sprawdzania jest złożona i chcesz ją wyekstrahować do osobnej funkcji
  2. Pracujesz z danymi z zewnętrznych źródeł (API, pliki JSON), których typ to unknown
  3. Chcesz stworzyć reużywalne narzędzia walidacji

Discriminated unions i exhaustive checks

Discriminated 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}

Exhaustive check z typem never

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.

Łączenie type guards - praktyczny przykład

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));

Podsumowanie

Type narrowing i type guards to fundamentalne narzędzia TypeScript, które pozwalają pisać bezpieczny kod przy jednoczesnym zachowaniu elastyczności union types:

  1. typeof - zawęża typy prymitywne (string, number, boolean)
  2. instanceof - zawęża typy klas
  3. in - zawęża typy na podstawie obecności właściwości w obiekcie
  4. Custom type guards (is) - definiujesz własne funkcje sprawdzające typ
  5. Discriminated unions - wspólne pole "tag" jednoznacznie identyfikuje wariant
  6. Exhaustive check z never - kompilator pilnuje, że obsłużyłeś wszystkie warianty

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."

Kod do tej lekcji: index.ts
1// Type narrowing i type guards - Park Jurajski
2console.log("=== TYPE NARROWING I TYPE GUARDS ===\n");
3
4// ===========================================
5// 1. typeof - zawezanie typow prymitywnych
6// ===========================================
7
8function formatDinoData(value: string | number | boolean): string {
9  if (typeof value === "string") {
10    return value.toUpperCase();
11  }
12  if (typeof value === "number") {
13    return value.toFixed(2) + " kg";
14  }
15  return value ? "Aktywny" : "Nieaktywny";
16}
17
18console.log("--- typeof ---");
19console.log(formatDinoData("Tyrannosaurus Rex"));
20console.log(formatDinoData(7500));
21console.log(formatDinoData(true));
22
23// ===========================================
24// 2. instanceof - zawezanie klas
25// ===========================================
26
27class Carnivore {
28  constructor(public name: string, public biteForce: number) {}
29  hunt(): string { return `${this.name} poluje z sila ${this.biteForce}N!`; }
30}
31
32class Herbivore {
33  constructor(public name: string, public food: string) {}
34  graze(): string { return `${this.name} zjada ${this.food}`; }
35}
36
37function handleDino(dino: Carnivore | Herbivore): string {
38  if (dino instanceof Carnivore) {
39    return dino.hunt();
40  }
41  return dino.graze();
42}
43
44console.log("\n--- instanceof ---");
45console.log(handleDino(new Carnivore("T-Rex", 12800)));
46console.log(handleDino(new Herbivore("Triceratops", "paprocie")));
47
48// ===========================================
49// 3. in - sprawdzanie wlasciwosci
50// ===========================================
51
52interface FlyingDino { name: string; maxAltitude: number; }
53interface SwimmingDino { name: string; maxDepth: number; }
54interface LandDino { name: string; runSpeed: number; }
55
56type AnyDino = FlyingDino | SwimmingDino | LandDino;
57
58function describeDino(dino: AnyDino): string {
59  if ("maxAltitude" in dino) return `${dino.name} lata do ${dino.maxAltitude}m`;
60  if ("maxDepth" in dino) return `${dino.name} nurkuje do ${dino.maxDepth}m`;
61  return `${dino.name} biega ${dino.runSpeed} km/h`;
62}
63
64console.log("\n--- in operator ---");
65console.log(describeDino({ name: "Pteranodon", maxAltitude: 500 }));
66console.log(describeDino({ name: "Mosasaurus", maxDepth: 200 }));
67console.log(describeDino({ name: "Velociraptor", runSpeed: 64 }));
68
69// ===========================================
70// 4. Custom type guard (is)
71// ===========================================
72
73interface DinoEgg { species: string; incubationDays: number; }
74interface HatchedDino { species: string; age: number; enclosureId: string; }
75
76function isHatched(s: DinoEgg | HatchedDino): s is HatchedDino {
77  return "age" in s && "enclosureId" in s;
78}
79
80console.log("\n--- Custom type guard ---");
81const egg: DinoEgg = { species: "Raptor", incubationDays: 45 };
82const baby: HatchedDino = { species: "Raptor", age: 10, enclosureId: "E-01" };
83console.log("Egg hatched?", isHatched(egg));
84console.log("Baby hatched?", isHatched(baby));
85
86// ===========================================
87// 5. Discriminated union + exhaustive check
88// ===========================================
89
90interface SecurityAlert { kind: "security"; zone: string; level: number; }
91interface MedicalAlert { kind: "medical"; dinoId: string; symptoms: string[]; }
92interface SystemAlert { kind: "system"; component: string; errorCode: number; }
93
94type ParkAlert = SecurityAlert | MedicalAlert | SystemAlert;
95
96function handleAlert(alert: ParkAlert): string {
97  switch (alert.kind) {
98    case "security": return `BEZPIECZENSTWO: strefa ${alert.zone}, poziom ${alert.level}`;
99    case "medical": return `MEDYCZNY: dino ${alert.dinoId}, objawy: ${alert.symptoms.join(", ")}`;
100    case "system": return `SYSTEM: ${alert.component}, kod ${alert.errorCode}`;
101    default:
102      const _never: never = alert;
103      return _never;
104  }
105}
106
107console.log("\n--- Discriminated union ---");
108console.log(handleAlert({ kind: "security", zone: "A", level: 9 }));
109console.log(handleAlert({ kind: "medical", dinoId: "D-01", symptoms: ["goraczka"] }));
110console.log(handleAlert({ kind: "system", component: "Ogrodzenie", errorCode: 500 }));
111
112console.log("\nType narrowing = bezpieczny kod z union types!");

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Który operator pozwala na zawężenie typu prymitywnego (np. string | number) w TypeScript?

Zadania praktyczne w grze

  • Edytor kodu

    Użyj typeof, instanceof i operatora in do zawężania typów w systemie Parku Jurajskiego

  • Klikanie w kolejności

    Ułóż elementy custom type guard function:

Przydatne artykuły