Co to są typy warunkowe?
Oczywiście sama nazwa sugeruje, że mamy to do czynienia z jakiegoś rodzaju warunkiem. Jeżeli programowałeś w jakimkolwiek języku, to z pewnością wiesz, że warunki odpowiadają za to, jak zachowuje się aplikacja i która część kodu powinna zostać wykonana w danym momencie.
Tak samo jest w tym przypadku, jednak zamiast kontrolowania zachowania samej aplikacji, lub strony internetowej, kontrolujemy system typów, który dostarcza nam TypeScript, dzięki czemu możemy tworzyć znacznie bardziej zaawansowane konstrukcje.
Kolejnym podobieństwem jest fakt, że bez warunków żaden język programowania nie może istnieć. Ta reguła sprawdza się również w TypeScript - niemal każda aplikacja wykorzystująca ten język, korzysta również z typów warunkowych. Istnieje duża szansa, że Ty również wykorzystujesz je w swoich projektach. Nawet jeżeli nie robisz tego bezpośrednio, to są one wykorzystywane w takich typach pomocniczych, jak Exclude
, czy Extract
.
Twórcy języka postarali się, żeby zastosowanie w praktyce tego elementu było jak najbardziej intuicyjne i wykorzystali dobrze znaną z JavaScript (i kilku innych języków) składnię: warunek ? a : b
, czyli innymi słowy operator trójargumentowy (ternary operator).
Warunki w praktyce
Teoria za nami, czas poznać typy warunkowe w praktyce. Zaczniemy od podstaw, czyli od samej składni:
Czas na wyjaśnienie co dzieje się w przykładzie powyżej:
Tworzymy alias typu o nazwie
Example
, do którego przypisujemy typ warunkowy.Warunek sprawdza, czy
"string literal"
(pamiętaj, że jest to nadal typ, a nie wartość) można przypisać do typu string. Używamy do tego składnix extends y
(czyx
można przypisać doy
)Następnie tworzymy dwie zmienne:
bool
ibool2
, które typujemy z użyciem utworzonego wcześniej typu.
Powyższy przykład należy zatem interpretować w następujący sposób: jeżeli "string literal"
można przypisać do typu string
, to alias Example
powinien być literałem typu boolean, a konkretnie true
. W innym przypadku powinien mieć przypisany typ false
.
Jak zapewne się domyślasz, zastosowanie, które pokazałem wyżej nie jest zbyt przydatne i ma na celu jedyne wprowadzić cię do składni. W praktyce typy warunkowe są jednak najczęściej wykorzystywane w połączeniu z typami generycznymi (artykuł o nich znajdziesz tutaj). Takie połączenie daje nam znacznie więcej możliwości i znacznie bardziej dynamiczne typy.
Czas na nieco bardziej rozbudowany przykład:
IsString
przyjmuje jeden argument generyczny, a następnie sprawdza, czy jest on typu string
. Jeżeli tak, to otrzymamy typ true
, w przeciwnym razie otrzymuje false
.
Możliwości, które daje nam takie połączenie są niemal nieograniczone. Oczywiście przykład, który pokazałem nadal jest dość prosty, dlatego postaram się teraz stworzyć coś nieco bardziej skomplikowanego, a przy okazji wykorzystać typy mapowane.
Tworzymy generyczny typ ReplaceValue
, który przyjmuje 3 argumenty generyczne:
Typ obiektowy
Jeden z kluczy podanego wcześniej typu
Dowolny typ
Założeniem tego typu jest możliwość przekazanie typu obiektowego, a następnie podmienienie jednej z jego wartości na nową. W typie Person
zamieniamy wartość przypisaną do klucza age
na unię string | number
.
Skupmy się jednak na samym typie warunkowym - w tym przypadku sprawdza on po kolei każdy klucz obiektu (ponieważ został zastosowany w typie mapowanym) i porównuje go z przekazanym argumentem generycznym TKey
, kiedy te dwie wartości do siebie pasują, podstawiany jest nowy typ (czyli trzeci argument TValue
), w przeciwnym nic nie zmieniamy i po prostu przekazujemy dotychczasowy typ.
Zanim przejdziemy dalej poruszę jeszcze temat przekształceń w typach mapowanych (jeżeli wcześniej nie spotkałeś się z tym terminem, to ponownie odsyłam do artykułu o typach mapowanych) z użyciem warunku.
Dzięki wykorzystaniu typów warunkowych możemy stworzyć bardzo zaawansowane przekształcenia, dopasowane do naszych potrzeb. Zacznijmy od przykładu:
Za pomocą Key extends "small" ? never : Key
jesteśmy w stanie odfiltrować wartości, które nie powinny znaleźć się w nowym typie - klucz o typie never
zostanie po prostu usunięty przez TS.
Zastosowań jest oczywiście znacznie więcej, niż tylko usuwanie kluczy, a jeżeli połączymy to z typami generycznymi, to otrzymamy niemal nieskończone możliwości 😎
Zagnieżdżone warunki
Do tej pory używaliśmy tylko jednego warunku w danym typie, ale nie ma żadnych przeciwwskazań, żeby użyć ich więcej, a nawet znacznie więcej.
Jeżeli zagłębimy się w typy stosowane w niektórych bibliotekach, to okaże się, że takie rozwiązanie jest stosowane bardzo często. Za przykład może tu posłużyć popularna paczka zod, która zawiera między innymi taki typ:
Ten typ stosuje wiele innych typów pomocniczych, dlatego nie będziemy się skupiać na jego działaniu, a raczej na samym zapisie i składni, którą tu zastosowano.
Jest tu wiele zagnieżdżonych typów warunkowych, ale dzięki temu, że autor zastosował zapis, w którym warunek, oraz jego możliwe wyniki są w osobnych liniach, to czytelność jest na wysokim poziomie (prawdopodobnie najwyższym, jaki można osiągnąć w TS).
Zwróć również uwagę, że w zapisie powyżej tylko w negatywnych przypadkach jest używany kolejny warunek - to również znacząco wpływa na poprawę czytelności, jednak nie zawsze jest to możliwe do uzyskania.
Czasami będzie potrzebować typu, który rozwidla się bardziej, jednak należy mieć świadomość, że zawsze odbędzie się to kosztem czytelności. Przykład poniżej ilustruje rozwidlone i zagnieżdżone typy warunkowe:
Jak widzisz czytelność tutaj jest już znacznie gorsza, a zrozumienie przepływu kodu wymaga czasu, dlatego zawsze, zanim zaczniesz tworzyć tak rozbudowane typy, zastanów się, czy nie można ich w jakiś sposób podzielić i tym samym zwiększyć czytelność.
Na diagramie poniżej przedstawiłem co dzieje się wewnątrz tego typu. Zielone strzałki odpowiadają spełnionemu warunkowi, natomiast czerwone wskazują co się stanie, gdy warunek nie zostanie spełniony.
Podsumowanie
Znasz już podstawowe założenia typów warunkowych, a także nieco bardziej zaawansowane konstrukcje językowe, takie jak zagnieżdżone typy warunkowe oraz wykorzystanie ich z typami generycznymi i mapowanymi.
Nie oznacza to jednak, że temat typów warunkowych jest wyczerpany, ponieważ nie wspomniałem do tej pory o słowie kluczowym infer
, które odgrywa znaczną rolę w świecie TypeScriptu. Jest to jednak na tyle rozbudowany i skomplikowany temat, że postanowiłem poświęcić mu osobny artykuł (w którym dodatkowo rozbudujemy wiedzę w oparciu o praktyczne przykłady).
Mimo wszystko, z obecną wiedzą powinieneś/powinnaś być w stanie tworzyć swoje własne typy warunkowe i eksperymentować z nimi, ponieważ praktyka jest (jak zawsze) kluczowa do zrozumienia niemal każdego zagadnienia w świecie programowania 🙂