Menu

Typ never w TypeScript: exhaustive check i never a void

never to typ bez żadnych wartości. Jest typem zwracanym przez funkcje, które nigdy się nie kończą, typem, który zostaje, gdy zawężanie wykluczyło każdy przypadek, i narzędziem stojącym za wyczerpującym sprawdzaniem w switch. Zobacz, skąd się bierze i czym różni się od void.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

never to typ bez żadnych wartości. Funkcja, której typem zwracanym jest never, nigdy nie kończy się normalnie: rzuca wyjątek albo działa w nieskończoność. Zmienna typu never oznacza kod, który nie może się wykonać, i to właśnie umożliwia wyczerpujące sprawdzanie przypadków.

return fail(...) kompiluje się w funkcji zwracającej number, bo never jest przypisywalny do każdego typu: wartość typu never nigdy tak naprawdę nie nadejdzie.

Funkcje, które nigdy nie zwracają

Są dwa rodzaje funkcji, które nigdy nie docierają do końca: taka, która zawsze rzuca wyjątek, i taka z nieskończoną pętlą. Kompilator sprawdza tę deklarację. Funkcja z adnotacją never, której koniec jest osiągalny, to błąd TS2534, A function returning 'never' cannot have a reachable end point.

function fail(message: string): never {
  throw new Error(message);
}

function runForever(): never {
  while (true) {
    // poll, serve requests...
  }
}

Wnioskowanie zależy od składni. Deklaracja funkcji, która tylko rzuca wyjątek, jest wnioskowana jako zwracająca void, a funkcja strzałkowa lub wyrażenie funkcyjne, które tylko rzuca, są wnioskowane jako never:

function f1() { throw new Error("x"); }       // () => void
const f2 = () => { throw new Error("x"); };   // () => never

Zawężanie traktuje wywołanie jako ślepy zaułek tylko wtedy, gdy wywoływana nazwa ma jawny typ zwracający never: deklarację funkcji z adnotacją : never, jak fail wyżej, albo zmienną z adnotacją typu, const fail: (m: string) => never = (m) => { throw new Error(m); }. Wywnioskowany never się nie liczy, podobnie jak const fail = (m: string): never => ..., gdzie adnotację ma tylko funkcja strzałkowa, a zmienna nie.

never a void

voidnever
Funkcja się kończytaknie (rzuca wyjątek lub działa w nieskończoność)
Wartość w czasie działaniaundefinedbrak: wywołanie nigdy jej nie daje
Kod po wywołaniuosiągalnynieosiągalny
Przypisywalny do innych typówtylko do void, unknown, anydo każdego typu
Typowe użyciecallbacki, obsługa zdarzeń, funkcje z efektami ubocznymifail(), assertNever(), nieskończone pętle

Praktyczną różnicę widać przy zawężaniu. Po if (!user) fail("no user") kompilator wie, że user jest zdefiniowany w następnej linii, tylko jeśli fail zwraca never. Przy typie zwracanym void zakłada, że wykonanie może być kontynuowane.

Exhaustive check z never

Każdy case instrukcji switch po unii zawęża wartość. Gdy każdy element zostanie obsłużony, w default zostaje never. Przypisanie tej wartości do zmiennej never zamienia "każdy przypadek jest obsłużony" w coś, co weryfikuje kompilator:

Teraz dodaj trzeci element do unii bez dodawania przypadku:

index.ts(18,26): error TS2345: Argument of type '{ kind: "triangle"; base: number; height: number; }' is not assignable to parameter of type 'never'.

Błąd wskazuje zapomniany element. Dodaj case "triangle": return (shape.base * shape.height) / 2;, a kod znów się skompiluje. Funkcja pomocnicza assertNever to wielokrotnego użytku wersja tego samego sprawdzenia, a jej throw nadal ma znaczenie w czasie działania: dane z JSON albo ze starszego klienta mogą zawierać kind, który według typów jest niemożliwy. Ten wzorzec to podstawa discriminated unions.

Zawężanie aż do never

To samo dzieje się przy każdym zawężaniu, nie tylko w switch. Gdy każda możliwość zostanie wykluczona, zmienna ma typ never:

Jeśli później rozszerzysz parametr do string | number | boolean | bigint, linia const nothing: never = x stanie się błędem i wskaże funkcję, którą trzeba zaktualizować.

never znika w uniach

never to pusty zbiór wartości, więc dodanie go do unii niczego nie zmienia: string | never to po prostu string. W ten sposób conditional types filtrują unie. Gałąź zwracająca never usuwa dany element:

Wbudowane Exclude<T, U> i Extract<T, U> działają dokładnie tak. W przecięciu jest odwrotnie: string & never to never.

Niemożliwe typy stają się never

Przecięcie, którego nie może spełnić żadna wartość, redukuje się do never:

type A = string & number;                   // never
type B = { kind: "a" } & { kind: "b" };     // never

Odczyt właściwości wartości typu B zgłasza przyczynę: Property 'kind' does not exist on type 'never'. The intersection 'B' was reduced to 'never' because property 'kind' has conflicting types in some constituents. Gdy zbudowany typ okazuje się never, szukaj dwóch części, które sobie przeczą.

never, unknown i any

TypJakie wartości zawieraPrzypisywalny doPrzyjmuje
unknownkażdą wartośćtylko unknown i anywszystko
anykażdą wartośćwszystkiego poza neverwszystko
neverżadnych wartościwszystkiegotylko never

unknown to szczyt hierarchii typów, a never jej dno. any w ogóle nie należy do hierarchii: wyłącza sprawdzanie.

Najczęściej zadawane pytania

Czym jest typ never w TypeScript?

never to typ, który nie ma żadnych wartości. Nie można do niego nic przypisać (poza innym never), a sam jest przypisywalny do każdego typu. Pojawia się jako typ zwracany przez funkcje, które zawsze rzucają wyjątek lub działają w nieskończoność, jako typ zmiennej po tym, jak zawężanie wykluczyło każdą możliwość, i jako wynik niemożliwych typów, takich jak string & number.

Czym różni się never od void?

Funkcja zwracająca void kończy się normalnie; po prostu nie zwraca użytecznej wartości (w czasie działania zwraca undefined). Funkcja zwracająca never w ogóle się nie kończy: rzuca wyjątek albo działa w nieskończoność. Kod po wywołaniu funkcji never jest nieosiągalny i TypeScript tak go traktuje przy zawężaniu.

Jak zrobić exhaustive check w TypeScript?

W gałęzi default instrukcji switch po unii przypisz wartość do zmiennej typu never albo przekaż ją do funkcji assertNever(value: never): never, która rzuca wyjątek. Jeśli każdy przypadek jest obsłużony, wartość ma tam typ never i kod się kompiluje. Jeśli brakuje przypadku, kompilator zgłasza, że brakujący element nie jest przypisywalny do never.

Dlaczego mój typ to never?

Zwykle dlatego, że TypeScript wykluczył każdą opcję przez zawężanie (na przykład po sprawdzeniu typeof x === "string" i typeof x === "number" dla string | number), albo dlatego, że przecięcie jest niemożliwe: string & number lub dwa typy obiektowe, których wspólna właściwość ma sprzeczne typy literałowe. Najedź na typ w edytorze, żeby zobaczyć, który krok go wytworzył.

Co znaczy "is not assignable to type never"?

Kod próbował umieścić prawdziwą wartość tam, gdzie dozwolone jest tylko never. W exhaustive check oznacza to, że jakiś element unii nie został obsłużony. W innych miejscach często znaczy, że tablica została wywnioskowana jako never[] albo przecięcie zredukowało się do never; dodaj adnotację lub popraw sprzeczne typy.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ