Blog na którym chcę dzielić się wiedzą i doświadczeniem.

Unknown, czyli dlaczego nie stosować typu any

Marek Szkudelski
Marek Szkudelski
Podstawowy

Wstęp

Ostatnio opublikowałem artykuł o tym, kiedy w praktyce można zastosować typ any w projekcie napisanym w TypeScript. Po wrzuceniu na Facebooka okazało się, że spotkał się on ze słuszną krytyką. (link do posta w grupie JS news - after hours - może się okazać, że nie masz dostępu). Chciałbym teraz podsumować wnioski z tej dyskusji i wyjaśnić, dlaczego nie miałem racji. Sam artykuł znajduje się tutaj. Raczej nie zalecam go czytać pod kątem merytorycznym, ale warto mieć kontekst tego, jak doszedłem do takich wniosków.

Na wstępie chciałbym podziękować wszystkim, którzy poświęcili czas na przeczytanie mojego poprzedniego artykułu i zostawienie komentarza z wyjaśnieniem, dlaczego moje rozwiązania są niepoprawne. Tutaj też pojawia się pozytywny wniosek z prowadzenia bloga. Publikując treści, dajemy innym szansę na zweryfikowanie naszej wiedzy. Gdybym trzymał wszystko to, czego się nauczę dla siebie, to możliwe, że nigdy bym się nie dowiedział, że nie mam racji. Możliwe nawet, że zastosowane przeze mnie błędnego rozwiązania spowodowałyby błąd w aplikacji, którą rozwijam w pracy. Dzięki prowadzeniu bloga i dzieleniu się wiedzą, mogę uniknąć pomyłek i sam czegoś się nauczyć.

O typie unknown

Typ unknown jest właściwie tym, na co wskazuje jego nazwa. Opisuje on wartość nieznaną nam podczas developmentu. Pod zmienną o takim typie może się kryć jakakolwiek wartość. Można więc zapytać, czym różni się on od any. Tym, że jest bezpieczniejszy. TypeScript nie pozwoli na wykonanie żadnej operacji na zmiennej z typem unknown, dopóki nie potwierdzimy, jaki to jest dokładnie typ. Można to zrobić na przykład dzięki mechanizmowi, który się nazywa type guards, czyli strażnicy typu. Jednak za nim o nich dokładniej opowiem, to spójrzmy, jak się zachowuje TypeScript, kiedy ma do czynienia z typem unknown.

let a: unknown = '123';
let b: number = a; // error
a = 123; // Ok

Po pierwsze, w powyższym przykładzie możemy zobaczyć, że TypeScript nie pozwoli nam na przypisanie czegoś, co jest unknown do zmiennej typu number. Pozwoli natomiast na przypisanie czegokolwiek do tej zmiennej. Dlaczego tak jest? Ponieważ mówimy TypeScriptowi, że nie wiemy, jaka może być tam wartość. Jeśli TS pozwoliłby nam przypisać wartość spod zmiennej a do zmiennej b, to moglibyśmy wywołać na tej wartości każdą metodę charakterystyczną dla numberu. Spowodowałoby to błąd w runtime'ie, a przed tym TypeScript powinien nas uchronić.

Kolekcja czegokolwiek

W artykule o any podałem przykład z tablicą zawierającą any oraz obiekt z wartościami o takim typie. Na pierwszy rzut oka typowanie może się wydawać prawidłowe i praktyczne. Jednak jest z nim jeden problem. Owszem z zewnątrz nie ma różnicy czy argument funkcji ma unknown, czy any. Natomiast z punktu widzenia programistów, który taką funkcję piszą i utrzymują, any może prowadzić do błędów.

Załóżmy, że piszemy funkcję, która sprawdza, czy dana wartość znajduje się w tablicy. Tak, wiem, że jest taka funkcja w czystym JS, ale chcemy mieć możliwość późniejszej rozbudowy funkcji o dodatkowe opcje, więc piszemy ją sami. Chcemy, żeby funkcja powiedziała nam czy wartość występuje przynajmniej raz w przekazanym zbiorze danych. Na pierwszy rzut oka poniższe typowanie jest poprawne. Nie obchodzi nas jaki typ ma tablica, ani wartość. Nie zależy nam też na tym, żeby typy były takie same (wtedy użylibyśmy typu generycznego). Co może pójść nie tak?

function includes(array: any[], value: any): boolean {
  return array.some(item => item === value);
}

console.log(includes([123], 123)) // true
console.log(includes([123], 13)) // false

Wyobraźmy sobie, że chcemy dodać opcję sprawdzania także tablic zagnieżdżonych w poszukiwaniu danej wartości. Implementacja mogłaby wyglądać tak:

function includes(array: any[], value: any): boolean {
  return array.some(
    item => item === value || includes(item, value),
  );
}

console.log(includes([123, [13]], 13)) // true

Jeśli element tablicy nie spełnia warunku, to wywołaj rekurencyjnie funkcję includes z tym elementem. Widzicie już błąd, który pokaże się w konsoli? Będzie to błąd: array.some is not a function. Programista rozwijający tę funkcję popełnił błąd. Nie sprawdził, czy item jest tablicą przed wywołaniem metody includes. TypeScript nie narzeka dlatego, że poinformowaliśmy go, że elementy tablicy mogą być czymkolwiek - również tablicą. Zobaczmy, jak można za pomocą poprawnego typowania zapobiec takim błędom.

Przede wszystkim zamieńmy typ argumentu array na tablicę o typie unknown. Od razu możemy zobaczyć błąd kompilatora:

Argument of type 'unknown' is not assignable to parameter of type 'unknown[]'.

TypeScript nie wie, czy item może być tablicą, więc nie pozwoli nam go przekazać do funkcji includes. Teraz wiemy o potencjalnym błędzie w runtime'ie, więc możemy się na niego przygotować, sprawdzając, czy element tablicy jest także tablicą. Ostateczna implementacja mogłaby wyglądać w taki sposób:

function includes(array: unknown[], value: unknown): boolean {
  return array.some(
    item => item === value || 
    Array.isArray(item) && includes(item, value),
  );
}

Inne przypadki

Błąd podobnego rodzaju jak ten, który opisałem powyżej, może zdarzyć się tak naprawdę wszędzie gdzie zostawimy any jak typ w naszym kodzie. Dlatego warto używać unknown zamiast any. Podobny przykład jak ten powyżej mógłbym pewnie znaleźć na przypadek mapowania kluczy z poprzedniego artykułu.

Jeśli chodzi o przykład z hookami w ReactJs, to nasuwa mi się pytanie, dlaczego twórcy tej biblioteki nie użyli unknown? Oczywiście nie wiem dlaczego, ale odpowiedź może być bardzo prosta. Mogło to wynikać z niewiedzy albo z niedopatrzenia programistów. Ludzie rozwijający publiczne projekty nie są nieomylni i też zdarzają im się pomyłki.

Trochę więcej o błędach w open source'owych projektach piszę w moim pierwszym artykule, gdzie analizuję kod źródłowy async pipe'a z Angulara.

Co natomiast z pozostałymi przypadkami użycia typu any, które opisałem w tamtym artykule?

Brak kontraktu API

Tak naprawdę totalny brak znajomości prawdopodobnego kontraktu API, to bardzo rzadki przypadek. Mam nadzieję, że nikomu to się na zdarza w pracy, ponieważ są to bardzo niekomfortowe warunki do pracy. Myślę, że w praktycznie każdej sytuacji możemy wcześniej dogadać się z backendem odnośnie do kontraktu. Jeśli nie, to możemy popełnić sami pewne założenia.

Prawdą jest, że nawet jeśli nie znamy kontraktu, to podczas implementacji i tak zakładamy na przykład, to jak będzie się nazywać pole, albo jakiego będzie typu. Takie założenia wystarczy przenieść do TypeScriptowego interfejsu. Na nasze szczęście współczesne IDE bardzo często oferują narzędzia do bardzo łatwego refaktoru takiego jak zmiany w nazwach pól. Więc w sytuacji, kiedy okaże się, że kontrakt jednak będzie nieco odbiegał od naszych założeń, to będziemy mogli w miarę prosty sposób dostosować się do tego.

Migracja z czystego JS

Jak natomiast sytuacja wygląda z migracją większego projektu z czystego JavaScriptu do TypeScripta? Kiedy dostajemy na twarz kilkaset błędów kompilacji, to aż się chce dać wszędzie any i skupić się na przyrostowym dodawaniu typów do naszego projektu.

Kiedy wrócimy do źródła, czyli oficjalnej dokumentacji TypeScripta i poczytamy o migracji projektu z JSa, to okaże się, że jest lepsza droga. Kompilator TypeScripta przyjmuje opcję konfiguracyjną "allowJs", którą możemy ustawić na true lub false. Dzięki włączonej takiej opcji możemy mieć pliki JavaScriptowe oraz TypeScriptowe i jednym projekcie. Kompilator sobie z tym poradzi i da nam działający projekt. W dalszym ciągu możemy dodawać typy przyrostowo. Na początek możemy zmienić tylko jeden plik z .js na .ts, dodać typy i dorzucić takie zmiany do repozytorium. W kolejnych dniach możemy zmieniać rozszerzenia kolejnych plików i typować je. Dzięki temu uchronimy się przed sytuacjami, kiedy pozostawimy gdzieś jakieś zapomniane any w przepisanym projekcie.

Podsumowanie

Odniosłem się do każdego przypadku użycia, który wymieniłem w poprzednim artykule. W przypadku każdego stwierdzam, że nie warto używać typu any. W ciągu około tygodnia zmieniło się totalnie moje zdanie na ten temat. Jak to możliwe? Dlatego, że zakładam, że prowadząc bloga, dostarczam wiedzę innym, ale sam też jestem otwarty na naukę.

Myślę, że każdy z nas w jakimś stopniu żyje w bańce swojego doświadczenia i wiedzy, która może nie być poprawna. Osoby, które chcą dzielić się wiedzą, powinny być gotowe na to, że ich bańka jest wystawiana na opinie innych. Przez te opinie ta bańka może pęknąć.

Artykuły, które też mogą Ci się spodobać:

Myślę, że typ `any` wywołuje sporo kontrowesji. Przez jednych jest nadużywany, a przez drugich w ogóle odradzany. Postanowiłem więc podsumować wszystkie znane mi przypadku użycia typu `any` w artykule.

PodstawowyTypeScript

Uważam, że Mapped Types są często niedocenianym elementem TSa, więc w artykule pokazuję kilka z tych typów oraz wchodzę do środka, żeby pokazać jak można budować swoje własne typy.

ŚredniozaawansowanyTypeScript
ładowanie punktów reputacji...

Newsletter

Podobają Ci się treści, którę tworzę?

Zapisz się na newsletter.

Więcej info.