Ten artykuł jest częscią serii

Jeśli nie przeczytałeś jeszcze poprzednich artykułów, to zawsze możesz to nadrobić klikając w przycisk.

Miniatura artykułu

TypeScript - typeguard, czyli strażnik typu

10 minut

Skopiuj link

Data publikacji: 7/2/2023, 2:11:09

Ostatnia aktualizacja: 7/27/2024

Typeguard? A na co to komu potrzebne?

W pewnych sytuacjach TypeScript nie może być w 100% pewny, co do typu z którym ma aktualnie do czynienia. Przykładów można dać tu kilka:

  • jest to typ generyczny (omówimy je w kolejnym artykule) i nie można go określić podczas kompilacji

  • funkcja może mieć parametr, który przyjmuje np. unię typów string | number

  • możemy pracować z typem unknown lub any (artykuł na temat tych dwóch typów znajdziesz tutaj)

W takich sytuacjach z pomocą przychodzi typeguard (w wolnym tłumaczeniu "strażnik" typu, ale trzymajmy się jednak angielskiej wersji). Z jego pomocą możemy doprecyzować, lub innymi słowy upewnić się, z czym tak naprawdę pracujemy.

Mimo, że nazwa może być dla Ciebie nowa, to sama konstrukcja języka już prawdopodobnie nie. W swojej najprostszej wersji typeguard, to po prostu instrukcja warunkowa. Istnieje również wersja z wykorzystaniem funkcji, ale to znacznie bardziej zaawansowany przypadek, dlatego omówimy go na samym końcu.

Składnia i praktyka

Zastosowanie poznamy na przykładzie funkcji - zatem posłużymy się drugim przypadkiem wymienionym na liście powyżej.

Utworzona funkcja ma jeden parametr - n, który może być liczbą lub stringiem (string | number). Następnie dodajemy do niego liczbę 2 i zwracamy wynik. Banalne, prawda?

Gdyby ten kod był napisany w JavaScript, to działałby bez najmniejszego problemu. Dlaczego więc TypeScript nie pozwala na zastosowanie operatora + do parametru który jest stringiem, lub numberem? Przecież dla obu wartości można go poprawnie wykorzystać.

Odpowiedź jest prosta - TS nie jest pewny z jaką wartością ma do czynienia wewnątrz funkcji addTwo i chroni nas przed błędnym działaniem. W przypadku przekazania argumentu, który jest liczbą, funkcja rzeczywiście doda do niego 2, i zwróci poprawny wynik. Problem pojawia się gdy przekażemy argument, który jest stringiem, np. "2", dojdzie wtedy do konkatenacji, a otrzymany wynik nie będzie poprawny.

Rozwiązaniem tego problem jest oczywiście typeguard. Zastosujemy najprostszą konstrukcję z użyciem instrukcji if...else.

Z wykorzystaniem operatora typeof jesteśmy w stanie "pomóc" zrozumieć TypeScriptowi na jakim typie operuje. Dążymy zatem do zawężenia typu i upewnienia się, że można na nim bezpiecznie wykonać daną operację.

Podsumowując: typeguard to nic innego jak określenie na różne konstrukcje językowe (funkcje, instrukcje warunkowe), które zawężają możliwy typ do tego stopnia, że można go bezpiecznie wykorzystać. Nie zawsze oznacza to, że jest to jeden konkretny typ, czasami może to być kilka różnych typów, które posiadają używaną przez nas funkcję lub właściwość (wtedy można ją bezpiecznie użyć, ponieważ istnieje w każdym z typów i nie ma ryzyka wystąpienia błędu).

Operatory używane w typeguardach

Do tej pory użyliśmy typeof w celu sprawdzenia jaki typ ma dana wartość. Pokażę Ci jeszcze jeden przykład z wykorzystaniem tego operatora, a później omówimy inne, używane równie często (lub tylko trochę rzadziej).

typeof

Jak już wiesz operator ten sprawdza po prostu z jakim typem mamy do czynienia. W niektórych sytuacjach jest on konieczny (przykład wyżej), jednak czasami można go zastąpić.

W kodzie poniżej zastosowałem dwa podejścia:

  • Za pomocą instrukcji warunkowej if, sprawdziłem czy podana wartość jest truthy

  • Również za pomocą if sprawdzam, czy dana wartość ma typ string.

Ponieważ tablica na której operujemy może mieć wewnątrz tylko dwa typy (string | null), to sprawdzanie, czy wartość jest truthy wystarcza. TypeScript jest w stanie domyślić się, że jeżeli wartość istnieje, to musi być ona typu string. Dodatkowo odsiewamy również puste stringi (które w JavaScript są wartością falsy) i nie wykonujemy na nich żadnej operacji, więc to podejście jest znacznie lepsze.

instanceof

Jen operator sprawdza się w przypadku pracy z wartościami, które są obiektami. Jeżeli chcesz upewnić się, czy dany obiekt jest instancją wskazanej klasy (lub klasą, która powstała na jej bazie), lub funkcji konstruktora, to jest to idealny wybór.

Jest to nieco bardziej skomplikowany przykład - używam w nim klas w celu przedstawienia działania operatora. Jeżeli ta składnia jest dla Ciebie obca, to koniecznie zapoznaj się z innymi artykułami w tej serii.

Sprawdzenie, które wykonujemy ma na celu dokładnie to samo co wcześniej - ustalenie z jakim typem pracujemy.

in

Rzadko spotykany operator w kodzie (szczególnie w JavaScript). Jego działanie polega na sprawdzeniu, czy wskazana właściwość występuje w obiekcie. Ten sam wynik można osiągnąć na kilka różnych sposobów, ale ten, jest po prostu najczytelniejszy.

Zastosujmy go w praktyce. Przypuśćmy, że funkcja, którą tworzymy przyjmuje jeden argument typu unknown. Jeżeli ten argument okaże się być obiektem, to chcemy sprawdzić, czy istnieje w nim właściwość name, jeżeli tak, to wypiszemy ją w konsoli.

Utworzyłem kilka zmiennych wewnątrz funkcji, żeby łatwiej było zrozumieć co się tak naprawdę dzieje:

  • isObject - ta zmienna przechowuje sprawdzenie typu user. Na tym etapie wiemy, czy user to obiekt lub null (w JS null rownież ma typ "object" - pozostałość po dawnych czasach, której nie można usunąć ze względu na wsteczną kompatybilność języka).

  • isNull - sprawdzamy, czy user to null

  • hasNameProperty - przechowuje sprawdzenie, czy właściwość name istnieje w obiekcie user

Oczywiście warunek ten można zapisać na wiele sposobów, również w jednej linii kodu. Do Ciebie należy decyzja, czy taki zapis jest czytelniejszy w danej sytuacji.

Typeguard jako funkcja

Czasami zwykłe sprawdzenie typu, pochodzenia instacji lub istnienia właściwości w obiekcie nie wystarczy i trzeba zastosować nieco bardziej skomplikowaną logikę. W takim przypadku utworzenie osobnej funkcji służącej do walidacji typu jest idealnym wyjściem.

Kolejnym dobrym powodem by utworzyć typeguard z wykorzystaniem funkcji jest reużywalność - jeżeli kilka razy sprawdzasz ten sam warunek, to prawdopodobnie lepiej jest utworzyć funkcję i użyć ją w kilku miejscach, zamiast duplikować kod.

Przejdźmy do praktyki. Dla ułatwienia powtórzymy przykład w którym sprawdzamy, czy właściwość name istnieje w obiekcie. Zasada o której należy pamiętać podczas tworzenia takiej funkcji to zwracana wartość - zawsze musi być to true lub false.

A teraz wyjaśnijmy po kolei co dokładnie dzieje się w tym kodzie:

  1. Tworzymy alias typu o nazwie User

  2. Następnie deklarujemy funkcję (typeguard) i za pomocą składni używanej do określenia typu zwracanego z funkcji oraz słowa kluczowego is (jego użycie jest wymagane, żeby TS zrozumiał, że ta funkcja jest typeguardem), dajemy znać TypeScriptowi, że wynikiem tej funkcji będzie wartość boolean określająca, czy przekazany argument jest wskazanego typu.

    Rozbijmy to jeszcze bardziej:
    user - to nazwa argumentu (zwróć uwagę, że jest pisany z małej litery)
    User - to alias typu (pisany z wielkiej litery)
    user is User - można to odczytać w ten sposób - zwrócona z funkcji wartość określi czy podany argument to obiekt spełniający warunki aliasu User.

  3. Następnie w instrukcji warunkowej if wywołujemy funkcję isUserObject i przekazujemy jej argument, których chcemy sprawdzić. Dzięki temu mamy typeguard, który można używać wielokrotnie.

Pamiętaj, że tworząc taką funkcję cała odpowiedzialność za poprawne utworzenie warunku sprawdzającego spoczywa na Tobie. W przypadku stosowania klasycznych typegaurdów TS jest w stanie (w większości przypadków) wykryć błędy w warunkach i tym samym nie pozwolić na odczytanie nieistniejących wartości.

Jako przykład powtórzymy jeszcze raz kod użyty wyżej. Tym razem zrobiłem jednak literówkę w nazwie właściwości.

Nadal jednak możemy bez problemu odczytać właściwość name (pomimo, że może ona nie istnieć w danym obiekcie). Dzieje się tak ponieważ TS nie sprawdza w żaden sposób kodu wewnątrz funkcji isUserObject, interesuje go jedynie zwrócona wartość. Jeżeli jest to true, to uznaje, że programista jest pewny, że dany obiekt jest typu User.

Szybkie podsumowanie

  • Typeguard to funkcja lub inne wyrażenie, które zawęża typ (nie zawsze do jednego konkretnego) i pozwala na bezpieczne korzystanie z niego. Jest nieodłącznym elementem podczas używania TypeScriptu i będziesz go wykorzystywać bardzo często.

  • Operatory, które najczęściej używa się w tego typu konstrukcjach to: typeof, instanceof oraz in. Możesz się również spotkać z użyciem Array.isArray(), Object.is() oraz kilku innych metod.

  • Typeguard może być utworzony za pomocą funkcji, dzięki czemu możemy używać go wielokrotnie i tym samym wyciągnąć skomplikowaną logikę walidacji do osobnej funkcji. Pamiętaj, że TS w żaden sposób nie sprawdza kodu wewnątrz takiej funkcji, interesuje go jedyne zwrócona wartość, dlatego koniecznie sprawdź dwa razy, czy warunek jest poprawny.

Avatar: Wojciech Rygorowicz

Software Engineer / Fullstack developer

Wojciech Rygorowicz

wojciech.rygorowicz@gmail.com

Podziel się na

Dodaj komentarz

Komentarze (0)

Brak komentarzy

Jeżeli zainteresował Cię ten artykuł koniecznie przeczytaj inne artykuły z tej serii