W TypeScripcie jest taki typ jak never. Jeśli pamiętasz matematykę z podstawówki, to były tam zbiory. Rysowało się takie okręgi. W zbiorach były jabłka, gruszki… nieważne 😆 Typ never oznacza pusty zboiór. Taki, w którym nic nie występuje. Zbiór ten nie wydaje się zbyt użyteczny. Są jednak sytuacje w programowaniu w TypeScript, gdzie warto wiedzieć jak z tego typu skorzystać, aby produkować kod bez bólu głowy.
Rozważmy taką funkcję:
const getMode = (value: 1 | 2) => {
// value // -> 2 | 1
if (value === 1) {
return 'normal';
}
// value // -> 2
}
Zaraz po wejściu do funkcji value może wystąpić w dwóch wartościach 1 lub 2. Umieszczam blok warunkowy if i sprawdzenie czy value to 1. W nim wywołuję return 'normal’. Co wg TypeScripta zwraca teraz funkcja? Jest to typ 'normal' | undefined
, czego oczekiwałem. Zaraz po wyjściu z bloku warunkowego value jest już ograniczony do wartości 2. W skrócie: nie wyczerpałem wszystkich możliwości, więc funkcja może nic nie zwrócić, czyli undefined. Istnieje taka możliwość.
Spróbuję zatem wyczerpać zakres wartości value kolejną instrukcją warunkową. Zmodyfikowana funkcja będzie wyglądać tak:
const getMode = (value: 1 | 2) => {
// value // -> 2 | 1
if (value === 1) {
return 'normal';
}
// value // -> 2
if (value === 2) {
return 'beast';
}
// value // -> never
}
Zaraz po wyjściu z drugiego ifa, value osiąga właśnie typ never. Co to oznacza? Generalnie kod, który się tu znajdzie nigdy nie powinien się wykonać. Ale spójrzmy jak TS widzi typ zwracany przez funkcję. Jest to 'normal' | 'beast' | undefined
. Dlaczego tak? Zapewne autorzy TypeScripta mają na to sensowne wytłumaczenie 😆
No dobra, ale przecież Ty, drogi programisto wiesz, że ta funkcja zwraca zawsze normal albo beast, bo więcej możliwości nie ma! Jak zatem powiedzieć TypeScriptowi, że to już koniec i może wyluzować z tym undefined?
Może zatrzymajmy się na chwilę i mimo wszystko spróbujmy to wziąć na logikę. Zastanówmy się, co musiałoby się wydarzyć, aby kod za drugim warunkiem miał się wykonać. Załóżmy sytuację, że ściągasz dane z jakiegoś API i jakimś cudem trafiła tu wartość 3. Teoretycznie to może się wydarzyć, gdyż TS nie działa w runtime i nie waliduje danych out of the box. Skoro TS nie zaprotestuje, to sami wyrzućmy błąd!
export const getMode = (value: 1 | 2) => {
// value // -> 2 | 1
if (value === 1) {
return 'normal';
}
// value // -> 2
if (value === 2) {
return 'beast';
}
// value // -> never
throw new Error(`Unexpected value: ${value}`);
}
Okazuje się, że TS pozwala na to. Teraz sami zadbaliśmy o obsługę ewentualnego błędu w runtime. Co się okazuje? Teraz w magiczny sposób TS domyślił się, że funkcja nie będzie wchodzić w ten block i na pewno nie zwróci undefined. Typ zwracany to już 'normal' | 'beast'
, tak jak oczekiwaliśmy pierwotnie.
Zakładam, że zachodzi potrzeba użycia tego mechanizmu ponownie. Dobrym pomysłem będzie więc stworzenie funkcji, która będzie taki błąd obsługiwać. Nazwę ją assertUnreachable. Zapamiętaj tę nazwę ponieważ możesz się z nią spotkać częściej. Jest to utarty termin, który dotyczy takiego zjawiska również w innych językach programowania.
Funkcja będzie wyglądać następująco:
const assertUnreachable = (unexpected: never) => {
throw new Error(`Unexpected value: ${unexpected}`);
}
Zwróć uwagę, że właśnie wykorzystałem mityczny typ never 😎 Jako never podałem zarówno argument jak i typ zwracany. Jako unexpected będziemy się nomen omen spodziewać wartości, która miałaby się tu nigdy nie znaleść. O to chyba najlepsze logiczne wytłumaczenie, co ten typ oznacza. Dodatkowo jawnie podaję never jako typ zwracany i w tym wypadku muszę już wyrzucić błąd. Funkcja która jawnie zwraca never powinna wyrzucić błąd. To następne świetne, krótkie i zwięzłe wytłumaczenie czym jest typ never.
Ostatecznie kod funkcji będzie wyglądał tak:
export const getMode = (value: 1 | 2) => {
if (value === 1) {
return 'normal';
}
if (value === 2) {
return 'beast';
}
return assertUnreachable(value);
}
Jaka jest dodatkowa korzyść, z takiej konstrukcji? Jeśli będziemy chcieli rozszerzyć typ argumentu do 1 | 2 | 3
, to TS wyrzuci błąd kompilacji, gdyż assertUnreachable może przyjąć tylko typ never, a w tym wypadku będzie to 3.
export const getMode = (value: 1 | 2 | 3) => {
if (value === 1) {
return 'normal';
}
if (value === 2) {
return 'beast';
}
// błąd, value może być tylko typu never, a jest 3
return assertUnreachable(value);
}
Dowiedziałem się o takiej funkcji z książki „TypeScript na Poważnie” Michała Miszczyszyna. Mam nadzieję, że Michał nie zabije mnie za tego spoilera 😅 Zapraszam też do obejrzenia mojej rozmowy live z Michałem.