Never, any i unknown
W trakcie pracy przemierzając projekty wykorzystujące TypeScripta można spotkać wiele koncepcji, patternów oraz ciekawych konstrukcji.
Jednakże, pomimo różnic pomiędzy nimi i tysięcy podejść developerów jedno jest niezmienne.
Mamy oczywiście na myśli typ any przypisany do niezliczonej liczby zmiennych i metod. Zupełnie inaczej ma się sytuacja w przypadku never oraz unknown. Szansa na spotkanie się z nimi, zwłaszcza w repozytoriach, gdzie commitują osoby z mniejszym doświadczeniem jest porównywalna z wygraniem na loterii
Zapewne jest Ci znana opinia, że korzystanie z any NIE jest najlepszą praktyką. Z kolei czy wiesz, jak sprawy się mają z never i unknown? Po dzisiejszym artykule mamy nadzieję, że z łatwością odpowiesz na to pytanie
Typ never
Wyjaśnienia rozpoczniemy od najbardziej, dziwnego z powyższej trójcy.
Tłumacząc jego działanie z typescriptowego na nasze to typ oznaczający, że jesteśmy w miejscu, w którym nie powinniśmy się znaleźć. Nie można, do niego niczego przypisać co intuicyjnie jest dość dziwne, ale ma to też swoje zastosowanie.
W takim razie, gdzie możemy go użyć?
Najbardziej powszechnym i prawidłowym przykładem są wszelkiego rodzaju funkcje, które wewnątrz rzucają jakiś błąd.
function testFunction(parameter: string): never { throw new Error("My custom Error"); }
Kolejnym, nieco bardziej egzotycznym przykładem użycia może być stworzenie typu z wykluczeniem
type ExcludeStrings<T> = T extends string ? never : T; function withoutStrings<T>(param: ExcludeStrings<T>) { console.log(param); } // ✔️ ok withoutStrings(123) // ❌ Argument of type 'string' is not assignable to parameter of type 'never' withoutStrings("abc")
Na powyższym fragmencie widać, że nowo powstały typ akceptuje wszystko poza stringami
Never warto także zastosować, gdy mamy do czynienia z nieskończonymi pętlami:
const infiniteLoop = (): never => { while (true) { console.log("I am endless"); } }
Reguła kciuka może być taka: Gdy widzisz gdzieś słowo kluczowe throw lub nieskończoną pętlę, to warto się zastanowić czy nie ma tam również miejsca na never.
Typ any
Pomimo złych opinii i nagonki any może być niezwykle przydatne. Całkiem możliwe, że to właśnie dzięki temu typowi ekosystem TypeScript’a tak szybko zyskał na popularności i jest sprawnie wdrażany nawet w wieloletnich projektach.
Do any możemy przypisać wszystko, a takiej zmiennej użyć wszędzie.
To trochę tak jakbyśmy powiedzieli TypeScriptowi: „Słuchaj, generalnie działaj normalnie w całym projekcie, ale na tą konkretną zmienną przymknij oko”
Brzmi jak marzenie, prawda?
Niestety nie z punktu widzenia bezpieczeństwa. Biorąc pod uwagę, jak działa JavaScript, istnieje duże prawdopodobieństwo przypadkowego zastosowania niedozwolonych operacji na wartościach np. wywoływania undefined jak funkcji lub operacji matematycznych na stringach
Skoro to takie zło to, dlaczego w ogóle jest dla nas dostępne i kiedy po nie sięgnąć?
Teoretyczna odpowiedź powinna brzmieć: Najlepiej nigdy nie korzystaj z any.
Projekty migrujące się na TypeScript z klasycznego JS-a często posiadają dużo naleciałości gromadzonych przez lata. Otypowanie wszystkich użytych zmiennych i funkcji zajęłoby wieczność.
Dzięki punktowemu „ignorowaniu” bezpieczeństwa możemy stopniowo przepisywać projekt, zachowując kompatybilność wsteczną i świat się nie zawali. Przyznasz, chyba że lepiej mieć nawet 20% prawidłowo otypowanego kodu niż 0%
Istnieją również sytuacje, w których faktycznie nie obchodzi nas, jaki typ występuje w danym miejscu. Przykładem są generyczne funkcje, w których nie wiemy, czy oraz ile argumentów przyjmą. Pamiętasz debounce z tego artykułu?
Zobacz, jak może wyglądać otypowana wersja:
function debounce(callback: (...params: any[]) => void, delay = 300) { let timeout: ReturnType<typeof setTimeout>; return (...args: any[]) => { clearTimeout(timeout); timeout = setTimeout(() => { callback(...args); }, delay); }; }
Czy w powyższym przykładzie można nadać inny typ dla …args lub …params?
Oczywiście, że można
Ale czy any[] nas bardzo boli?
W tym przypadku nie
Typ unknown
Ostatni z tytułowych bohaterów, pomimo że raczej rzadko używany, wprowadzony do projektu przyniesie szereg korzyści.
Podobnie jak poprzednik przyjmie wszystko, ale to, co zasadniczo odróżnia unknown od any to fakt, że zmiennej tak otypowanej nie możemy nigdzie użyć.
W ten sposób mówimy TypeScriptowi: "W tym momencie nie mam pojęcia, co to jest, bądź ostrożny"
Pomimo „mechanizmu ochronnego” do takiej zmiennej możemy się dobrać. Aby tego dokonać, najpierw należy wprowadzić do akcji Type Guardy. Cały temat guardów zasługuje na osobny wpis, ale tutaj użyjemy jednego bardzo dobrze znanego, czyli typeof.
let variable: unknown = 'instytut'; if (typeof variable === 'string') { variable.toUpperCase() }
Jak widzisz, brzmi skomplikowanie, a w rzeczywistości w kodzie takich fragmentów jest wiele. Cała koncepcja Type guarda w tym przykładzie sprowadza się do zastosowania starego dobrego if’a, wewnątrz którego sprawdzimy przy pomocy typeof typ wartości aktualnie przechowywanej w zmiennej z anotacją unknown
Co więcej, możesz posłużyć się typem unknown, gdy napotkasz trudności, stosując asercję.
Taka sytuacja nie przychodzi Ci do głowy? Spójrz na poniższy przykład
const functionWithAssertion = (param: Date) => { // ❌ Conversion of type 'Date' to type 'string' may be a mistake because neither type sufficiently overlaps with the other const x: string = param as string } functionWithAssertion(new Date())
W wyniku tego, że obiekt Date nie jest podtypem stringa ani odwrotnie otrzymujemy błąd. Możemy sobie z nim poradzić za pomocą tricku: as unknown as string
const functionWithAssertion = (param: Date) => { // ✔️ ok const x: string = param as unknown as string }
Dzięki temu, że do unknown możemy przypisać wszystko, a także unknown może być przypisane do wszystkiego kompilator przestał narzekać
Typy jako zbiory
Jeśli porównany typy do zbiorów to any oraz unknown zawierają wszystkie elementy. To takie worki bez dna, każda wartość pasuje.
Z kolei never jest zbiorem pustym. Jest workiem z dziurą, do którego żadna wartość nie pasuje
Zobrazujmy to porównanie
Powyższa grafika powinna pomóc Ci oswoić się metaforą zbiorów. W tym kontekście możesz jeszcze zetknąć się z określeniem top i bottom types. Top types to zbiór zawierający wszystkie możliwe wartości (any i unknown) a bottom type to zbiór pusty (never)
Podsumowanie
Po zapoznaniu się, z tym wpisem posiadasz już wystarczającą wiedzę na temat typów any, never i unknown. Gdy po raz kolejny ujrzysz w kodzie zagubioną zmienną i przyklejone do niej: any doskonale sobie poradzisz.
Nawet funkcje rzucające błędem czy używanie zmiennych unknown nie powinny stanowić dla Ciebie większego "typescriptowego" wyzwania