Język Typescript cieszy się coraz większą popularnością zarówno wśród projektów front-end jak i back-end, gdzie do tej pory wykorzystywany był Javascript. Nie ma w tym nic dziwnego – język ten oferuje nam dużo większą przewidywalność, stabilność oraz poprawia jakość kodu i projektu. Jedną z technik potrzebnych do organizacji kodu i jego lepszego zrozumienia, którą przybliżę w artykule, jest korzystanie z Type Guard.
Type Guard
Type Guard to blok kodu, który wykorzystując zawarte w nim warunki, zwróci informacje o typie przekazanej zmiennej. Pozwala nam to zawęzić typ zmiennej i poprawne wykorzystanie jej właściwości w dalszych częściach kodu. Aby zobrazować ten problem, przyjrzyjmy się poniższemu fragmentowi kodu.
Nasza aplikacja pozwala na zalogowanie się 3 typom użytkowników:
- Super admin,
- Admin,
- User.
Każdy z nich posiada wspólny typ Account oraz dedykowany i z unikalną metodą typ makeOrder, removeUser i removeAdmin:
Wygląda na to, że ma to ręce i nogi. Teraz użytkownik loguje się do naszej aplikacji i ma 3 ścieżki:
- złożenie zamówienia,
- usunięcie użytkownika,
- usunięcie admina.
Stwórzmy więc funkcję, która zadecyduje o tym, jaka metoda ma zostać odpalona:
Funkcja makeAction przyjmuje parametr account o unii User, Admin i SuperAdmin. Jak widać, Typescript informuje nas o polach dostępnych na tym obiekcie, wynikających z części wspólnej typów TUser, TAdmin oraz TSuperAdmin. Co z metodami, które są dostępne na każdym z tych typów? Tutaj Typescript chroni nas przed błędnym wywołaniem metody, kiedy typ użytkownika będzie inny niż zakładamy.
Naszym zadaniem jest rozpoznanie tego typu i na jego podstawie, wywołanie odpowiedniej metody, która nie wywoła błędu w runtime.
Sposoby wyciągnięcia typu zmiennej
Istnieje 5 sposobów na wyciągnięcie typu zmiennej:
- użycie in w celu sprawdzenia istnienia property i określenia typu,
- użycie instanceof do określenia typu,
- użycie typeof do określenia typu,
- zawężanie poprzez porównanie,
- niestandardowe, własne type guard.
In type guard
In type guard dostarcza informacji, czy obiekt, który sprawdzamy, ma w sobie określone property. Należy pamiętać, że property znajdująca się w naszym warunku, powinna być unikalna dla jednego konkretnego typu, aby zawęzić wyboru dla Typescripta do jednej opcji (chyba, że cel jest inny). Jeśli warunek zostanie spełniony, Typescript zawęża wybór z 3 różnych typów do jednego, co spowoduje możliwość wywołania konkretnej metody, na konkretny typie obiektu.
Warto przypomnieć, że property musi być unikalne dla danego typu. Na przykład, jeśli warunkiem do wywołania metody makeOrder jest posiadanie property name, Typescript napotka błąd, ponieważ name występuje na każdym możliwym typie na obiekcie account.
Instanceof type guard
Instanceof jest wbudowanym w Javascript operatorem, sprawdzającym czy nasza zmienna jest instancją wybranej przez nas klasy. Dzięki temu jesteśmy w stanie zweryfikować, czy nasz parametr pokrywa się z wybraną przez nas klasą i zawęzić go do interesującego nas typu. Na początku zadeklarujemy klasy, które implementują nasze typy:
Następnie w warunku if sprawdzamy, czy nasz parametr jest instancją danej klasy. Metoda ta opiera się na porównaniu łańcuchów prototypów (prototype chain) dla zmiennej account oraz klasy np. User. W przypadku, gdy warunek będzie spełniony i łańcuchy będą się pokrywać, Typescript pozwoli nam na skorzystanie z metod i pól dostępnych tylko na tym konkretnym typie.
Efekt jest następujący:
The typeof type guard
Typeof, tak jak instanceof, jest wbudowanym operatorem Javascript. W tym przypadku nie porównujemy obiektu do instancji danej klasy, ale porównujemy typ na jego konkretnym polu i o wyniku informujemy Typescript. Aby zilustrować ten przykład, dodajmy nowe pole na typie TAccount o nazwie email, które może przyjąć wartość string lub null, jeśli użytkownik nie wprowadził jeszcze swojego maila do bazy danych.
Po wejściu na konkretny widok użytkownik ma możliwość wysłania maila z aktualną ofertą na swoją skrzynkę internetową. Stwórzmy zatem funkcję sendEmail, która przyjmuje parametr o wartości string oraz funkcję, która wywoła tę metodę. Jak widać, Typescript słusznie nie pozwoli nam na odpalenie kodu, ponieważ na tym etapie nie wiemy, czy przekazana przez nas wartość na pewno jest stringiem i program wyrzuciłby błąd w runtime.
Należy zatem dodać warunek sprawdzający, czy typ na polu email jest równy string.
W ten sposób mamy pewność, że pole email zawiera email użytkownika (podlegający wcześniejszej walidacji w aplikacji) i Typescript będzie zadowolony.
Zawężanie poprzez porównanie
W przypadku, kiedy jedno z pól na naszym obiekcie ściśle określa jego typ, warto zastosować zawężenie typu poprzez porównanie jego wartości. W naszym przypadku porównamy property type do każdej z wartości enuma AccountType. W ten sposób jesteśmy w stanie zawęzić typ obiektu do jednego konkretnego, który nas interesuje.
Po spełnieniu warunków, Typescript daje nam dostęp do wybranych metod dla każdego z trzech typów.
Niestandardowe type guards
Ciekawym rozwiązaniem na zawężanie typów jest pisanie własnych type guards. Zaletą tej techniki, jest możliwość stworzenia własnej metody, która nie narzuca nam żadnych ograniczeń i pozwala sprawdzić kilka interesujących nas warunków, a następnie zawęzić typ do jednego konkretnego, który nas interesuje. Niestety, jest tutaj pewne ryzyko popełnienia błędu podczas pisania takiego kodu i otrzymania błędu w runtime.
Przejdźmy zatem przez konkretny case, kiedy możemy tej techniki użyć. Dodajemy nowe pole, które przyjmuje wartość boolean o nazwie isAdult, na type TUser. Następnie dodajemy kolejny typ Tadult, który musi posiadać wartość isAdult równą true, oraz metodę adultAction dostępną tylko na tym typie.
Następnie stwórzmy metodę makeOnlyAdultAction, która ma za zadanie wywołać metodę adultAction, tylko dla osoby pełnoletniej.
Jak widać, Typescript wyrzuca nam błąd, ponieważ nie każdy zalogowany użytkownik, może być dorosły i nie jest w stanie wywołać tej metody.
Aby sprawdzić, czy dana osoba jest pełnoletnia, oraz aby Typescript wiedział, że może wywołać wyżej wymienioną metodę, stwórzmy więc nową metodę, która zawęzi nam typ TUser do typu TAdult.
Wyżej umieszczony kod zawiera 2 warunki:
- czy property type jest równe User,
- czy flaga isAdult jest równa true.
Jeśli warunek zostanie spełniony, typ, który przekazaliśmy do metody, zostanie przez Typescript zawężony do typu TAdult. Sprawdźmy, jak to wygląda w praktyce.
Podsumowanie
W artykule przedstawiłem kilka metod na zabezpieczenie swojego kodu i wzmocnienie bezpieczeństwa typów w naszym kodzie, dzięki czemu jest on bardziej stabilny, przewidywalny i częściowo przetestowany.
Należy pamiętać, że każda linijka ma swoją cenę. Dodanie type guards w wielu miejscach naszej aplikacji z pewnością przyniesie liczne korzyści w przyszłości, ale czas poświęcony na pokrycie naszego kodu type guardami będzie odczuwalny i wymaga dobrego przemyślenia i zaprojektowania typów/interfejsów. Projekt projektowi nierówny, lecz uważam, że gra jest warta świeczki.
Zostaw komentarz