Zawężanie typów to ustalanie przez TypeScript bardziej konkretnego typu wartości w danym miejscu kodu na podstawie sprawdzeń, które kod już wykonał. Parametr string | number staje się string wewnątrz if (typeof x === "string") i number w else.
Wywołanie value.toFixed(2) przed sprawdzeniem byłoby błędem kompilacji, bo toFixed nie istnieje w string. Sprawdzenie to zwykły JavaScript wykonywany w czasie działania programu, a zawężanie to kompilator, który je odczytuje i dopasowuje typ. Nic dodatkowego nie jest emitowane.
Analiza przepływu sterowania
TypeScript śledzi każdą ścieżkę przez funkcję: if/else, wczesne return i throw, switch, pętle oraz operatory skrócone &&, ||, ?? i ?:. W każdym miejscu typ zmiennej to to, co nadal jest tam możliwe.
Styl z wczesnym return ("guard clauses") to najczytelniejszy sposób zawężania: najpierw obsłuż nietypowe przypadki, a reszta funkcji pracuje na czystym typie.
Wszystkie sposoby zawężania
| Forma | Przykład | Zawęża |
|---|---|---|
| typeof | typeof x === "string" | typy proste i funkcje |
| Truthiness | if (x) | usuwa null, undefined i literały falsy |
| Równość | x === "a", x == null, x !== undefined | literały, null, undefined |
in | "swim" in pet | unie obiektów, według właściwości |
| instanceof | err instanceof TypeError | instancje klas |
Array.isArray | Array.isArray(x) | tablice kontra cała reszta |
| Przypisanie | x = 5 | do przypisanego typu |
| Predykat typu | function isUser(x: unknown): x is User | wszystko, co da się sprawdzić |
| Funkcja asercji | function assertUser(x: unknown): asserts x is User | wszystko po wywołaniu |
| Właściwość dyskryminująca | switch (shape.kind) | unie ze znacznikiem |
Ostatnie trzy omawiają strony o type guards i discriminated unions. Pozostałe są opisane niżej.
Zawężanie przez truthiness
if (x) usuwa null i undefined (oraz typy literałów false, 0, ""). Jest krótkie i ma jedną klasyczną pułapkę: 0 i "" są falsy, więc poprawne wartości są traktowane jak brakujące.
Liczby i stringi porównuj jawnie z undefined albo null (albo użyj ??). Truthiness sprawdza się dla obiektów, tablic i funkcji, które nigdy nie są falsy.
Zawężanie przez równość
===, !==, == i != zawężają obie strony. Porównanie z literałem zawęża do tego literału. == null (luźna równość) dopasowuje w jednym sprawdzeniu zarówno null, jak i undefined i to jedyne miejsce, gdzie luźna równość jest idiomatyczna.
Porównanie dwóch zmiennych zawęża obie do tego, co mogą mieć wspólnego: jeśli a: string | number i b: string | boolean przejdą a === b, obie mają w if typ string.
Operator in
"key" in obj zawęża unię typów obiektów do tych elementów, które mają (albo mogą mieć) tę właściwość.
in działa też na unknown, gdy wiesz już, że to obiekt: po typeof v === "object" && v !== null && "id" in v TypeScript wie, że v ma właściwość id typu unknown. W uniach, które projektujesz sam, wspólna właściwość-znacznik (kind: "fish") jest czytelniejsza niż sprawdzanie metod: ten wzorzec to discriminated union.
Zawężanie przez przypisanie
Zmienna ma typ zadeklarowany i typ zawężony, który podąża za jej przypisaniami. Przypisanie wartości zawęża zmienną do typu tej wartości, w granicach typu zadeklarowanego.
Gdzie zawężanie ginie
Zawężanie jest lokalne i ostrożne. Kilka sytuacji je resetuje:
- Inne wyrażenie. Sprawdzenie
obj.namezawężaobj.name(iobj["name"]), ale nieobj[key], gdykeyto zmienna typustring, a nie literał, i nie kopię zrobioną przed sprawdzeniem. - Callbacki i ponowne przypisanie. W callbacku zawężona zmienna
letzachowuje zawężenie tylko wtedy, gdy po utworzeniu callbacka nie dostaje nowej wartości.constalbo parametr, któremu nigdy nie przypisuje się nowej wartości, pozostaje zawężony. - Sprawdzenia ukryte w funkcjach pomocniczych. Funkcja
isString(x: unknown): booleannic nie mówi kompilatorowi. Nadaj jej typ zwracany w postaci predykatu typu,x is string, a jej wywołania będą zawężać tak jaktypeof.
Kompilator zgłasza index.ts(5,38): error TS18048: 'x' is possibly 'undefined'. Callback mógłby się wykonać później, po x = undefined. Usuń to ostatnie przypisanie (albo skopiuj wartość do const wewnątrz if), a kod się skompiluje i dwa razy wypisze 5.
Funkcję pomocniczą, która zwraca boolean, można naprawić, deklarując, czego dowodzi. Tym właśnie jest type guard:
function isString(value: unknown): value is string {
return typeof value === "string";
}
Od TypeScript 5.5 kompilator wnioskuje takie predykaty dla prostych funkcji strzałkowych, dlatego list.filter((x) => x !== undefined) zwraca teraz tablicę bez undefined.
Najczęściej zadawane pytania
Czym jest zawężanie typów w TypeScript?
Zawężanie to doprecyzowanie przez TypeScript typu zmiennej w bloku na podstawie sprawdzenia, które wykonuje kod. Po if (typeof x === "string") wartość string | number jest w if tylko string, a w else tylko number. Kompilator śledzi if, else, return, switch, &&, || i ?:, żeby ustalić typ w każdym miejscu, i nazywa się to analizą przepływu sterowania (control flow analysis).
Dlaczego TypeScript nie zawęża mojego typu?
Częste przyczyny: sprawdzenie dotyczy innego wyrażenia niż to, którego używasz (sprawdzane obj.a, używane obj[key] z key typu string); wartość to let, której przypisano nową wartość po utworzeniu callbacka, więc callback traci zawężenie; albo sprawdzenie jest ukryte w funkcji pomocniczej, która zwraca zwykły boolean zamiast predykatu typu x is T.
Czy zawężanie typów działa w czasie wykonania?
Sprawdzenia tak: typeof, instanceof, in i === to zwykły JavaScript, który się wykonuje. Samo zawężanie działa tylko w czasie kompilacji. TypeScript odczytuje twoje sprawdzenia i dopasowuje do nich typy statyczne, a do wyemitowanego JavaScriptu nic nie jest dodawane.
Jak zawęzić typ unknown w TypeScript?
Tymi samymi sprawdzeniami: typeof value === "string", Array.isArray(value), value instanceof Date, a dla obiektów typeof value === "object" && value !== null && "id" in value. Do sprawdzeń wielokrotnego użytku napisz funkcję type guard z typem zwracanym value is T.
Jak odfiltrować undefined z tablicy w TypeScript?
Od TypeScript 5.5 items.filter((x) => x !== undefined) zwraca T[] bez undefined, bo kompilator wnioskuje callback jako predykat typu. W starszych wersjach napisz predykat sam: items.filter((x): x is T => x !== undefined).