Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Type narrowing i type guards

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

Ir a CodeWorlds