Typescript – typ „never”

Typescript posiada typ never reprezentujący wartości które nigdy nie występują. Używamy go wszędzie tam gdzie wiemy że coś się nie wydarzy lub nie zakończy. Brzmi skomplikowanie ale w istocie takie nie jest, uwierz mi na słowo 🙂

Najprostszy przykład: funkcja która zawsze zgłasza wyjątek, nigdy się nie kończy – wyjątek powoduje natychmiastowe z niej wyjście – więc zwracany przez nią typ to never. Ale czy zawsze? Po co ustawiać ten typ? Jakie jest zastosowanie typu never?

Funkcje które nic nie zwracają

Omawiany typ najłatwiej wytłumaczyć przez pryzmat funkcji która nigdy się nie kończy. Oto przykład:

// Definicja funkcji: () => never function makeError(): never { throw new Error('Error'); // zgłoszenie wyjątku powoduje przerwanie funkcji } // Definicja funkcji: () => never function mainLoop(): never { while (true) { } } // Definicja funkcji wywnioskowana przez interpreter: () => never const makeError2 = () => { throw new Error('Error 2); };

Jak widać powyższe funkcje nigdy się nie kończą – zostaje zgłoszony wyjątek (co powoduje natychmiastowe funkcji opuszczenie) lub mamy do czynienia z niekończącą się pętlą. Na szczęście w większości przypadków nie musisz określać tu typu never zostanie określony przez Typescript – czyli inferowany.

Różnica pomiędzy „never” a „void”

Na początku powiedziałem, że never używamy w funkcjach które nic nie zwracają. Czym zatem różni się od void który przecież też mówi że funkcja nic nie zwraca? Tutaj właśnie nie do końca jest to prawda, typ void może przyjąć tzw. „wartość zerowalną” (z ang. „nullable”) – undefined lub null.

function sayNullable(): void { return ; // ok, ta funkcja zwraca "undefined" } function sayNever(): never { return ; // Error: type "undefined" is not assignable to type "never" }

W przypadku typu void wszystko jest ok, ponieważ funkcja zwraca wartość zerowalną (w zasadzie gdyby usunąć return to też by tak było, funkcja zwróci undefined).

Funkcja która ma zdefiniowany typ never nie może nic zwrócić, nawet wartości undefined. Tego typu funkcja nie ma standardowego przebiegu, może zgłosić wyjątek lub wogóle się nie zakończyć.

Zmienne z nieosiągalnymi typami

Oprócz funkcji, omawiany typ może zostać określony przez ochronę typów, przykład:

function testNever(value: number|string) { if (typeof value === "string" && typeof value === "number") { // value: never -> w tym bloku, zmienna ma typ never } else if (typeof value === "number") { // value: number -> w tym bloku, zmienna ma typ number } }

Mamy tu zmienną która może być ciągiem znaków jak i liczbą, jednak tam gdzie warunek nigdy się nie sprawdzi ustawiany jest typ never.

Inferencje typów dla deklaracje funkcji

Jak widzieliśmy na samym początku, inferencja potrafi poprawnie określić typ never w funkcjach. Jest tylko jeden haczyk – działa tylko w przypadku strzałek i wyrażeń funkcji, a więc nie działa dla deklaracji funkcji.

// Definicja funkcji: () => void function mainLoop() { while (true) { } }

Jak widzisz Typescript nie wywnioskuje tutaj typu never mimo że funkcja nigdy się nie kończy – dzieje się tak a żeby zachować kompatybilność wstecz. Wyjaśnienie tego wymaga wincyj kodu…

Jeśli za czasów EcmaScript 5 tworzyłeś „pseudo klasy” bazujące na funkcjach być może tworzyłeś też metody „pseudo abstrakcyjne” które to pilnowały braku ich implementacji – ot taki zamiennik słowa kluczowego abstract w Typescript (przydatny m.in. we wzorcu projektowym adapter). W przełożeniu na klasy w TypeScript wyglądałoby to tak:

class BasePlayer { // Metoda play którą trzeba zaimplementować aby użyć tego playera play() { throw new Error('Method "play" is not implemented'); } } class NetflixPlayer extends BasePlayer { // Konkretna implementacja metody play play() { // implementacja metody play dla Netflix } } // Zadziała bo metoda została przysłonięta new NetflixPlayer().play(); // Zgłosi wyjątek, nie można użyć tej klasy bezpośrednio! new BasePlayer().play();

Jest klasa bazowa BasePlayer. Ma metodę play pilnującą aby nikt nie zapomniał jej zaimplementować – zawsze zgłasza wyjątek. Klasa NetflixPlayer przesłaniając metodę play powoduje że wszystko działa – brak wyjątku.

Teraz wyobraź sobie że Typescript wnioskuje typ never w metodzie BasePlayer.play() – robiłby to prawidłowo, wszak ta metoda zawsze zgłasza wyjątek. W takim wypadku metoda NetflixPlayer.play() również musiałaby zwrócić typ never aby być kompatybilna z klasą rodzicem. Jak? np. zawsze zgłaszać wyjątek – ale jak ją wtedy zaimplementować? absurd.

Na szczęście tak się nie dzieje – jest zachowana kompatybilność wstecz i w powyższym przykładzie metoda domyślnie zwróci typ void, chyba że sam ustalisz inaczej.

Podsumowanie – do czego przydaje się typ „never”?

Zdawać by się mogło że never jest mało przydatny. Dzięki inferencji nie musisz wogóle o nim myśleć – istnieje, ale ta informacja najczęściej jest zbędna.

Jedyne sensowne zastosowanie widzę w typach warunkowych. Dokładniej chodzi mi o odrzucenie w momencie kiedy warunek nie zostaje spełniony, na przykład:

type NonNullable<T> = T extends null | undefined ? never : T;

Powyższy przykład nie dopusza aby T miał wartość zerowalną, w przeciwnym wypadku zwraca typ never. Przykład o tyle ciekawy że wzięty z definicji typów do ES5.

Jeśli znasz jeszcze jakieś ciekawe zastosowanie never – podziel się, jestem ciekaw 🙂

Dodaj komentarz

Twój adres email nie zostanie opublikowany.