Typy warunkowe w TypeScripcie pozwalają opisać sytuacje, w których typ wyniku zależy od innego typu, a nie od wartości w runtime. To szczególnie przydatne przy helperach, bibliotekach, adapterach API i kodzie, w którym JavaScriptowe dane są tylko częściowo przewidywalne. W praktyce chodzi o to, żeby kompilator pomagał mi wcześniej wychwycić błędy i jednocześnie nie zmuszał do ręcznego dopisywania zbędnych adnotacji.
Najkrócej: to narzędzie do opisywania zależności między typami
- Warunek ma postać `T extends U ? X : Y`, ale sprawdza zgodność typów, a nie dziedziczenie.
- `infer` pozwala wyciągać fragment typu, na przykład element tablicy albo typ zwracany przez funkcję.
- Przy uniach typów warunek zwykle rozdziela się na osobne gałęzie dla każdego składnika.
- Wiele wbudowanych utility types, jak `Awaited`, `ReturnType` czy `Extract`, bazuje na tym samym mechanizmie.
- Największe ryzyko to nadmierne skomplikowanie kodu typu, który po miesiącu staje się trudny do odczytania.
Czym są typy warunkowe i po co w ogóle istnieją
Ja traktuję je jako sposób na zapisanie logiki typu w formie małego „if/else” działającego wyłącznie na poziomie kompilatora. To nie zmienia działania JavaScriptu w przeglądarce ani na serwerze, ale pozwala TypeScriptowi lepiej modelować reguły domenowe, zwłaszcza wtedy, gdy jeden typ wyraźnie wynika z drugiego.
W dokumentacji TypeScript ten mechanizm jest opisywany jako jedna z technik budowania nowych typów z istniejących. I to jest dobre uproszczenie: zamiast tworzyć kilka luźnych wariantów ręcznie, można opisać zależność raz, a potem pozwolić kompilatorowi samemu wybrać właściwą gałąź. Właśnie dlatego typy warunkowe są tak użyteczne w kodzie, który ma sporo wspólnych klocków, ale różni się detalami zależnie od wejścia.
Najważniejsze jest jednak to, żeby nie mylić ich z logiką runtime. Jeśli warunek zależy od wartości, a nie od typu, to conditional types nie rozwiążą problemu. Gdy to rozróżnienie jest jasne, łatwiej przejść do tego, jak TypeScript faktycznie wybiera odpowiednią gałąź.

Jak działa warunek na poziomie typów
Podstawowa składnia jest krótka: T extends U ? X : Y. Najważniejszy szczegół brzmi jednak tak: extends w tym miejscu nie oznacza dziedziczenia, tylko sprawdzenie, czy jeden typ można przypisać do drugiego. To różnica, która na początku często myli nawet osoby znające TypeScript z codziennej pracy.
type IsString = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString; // false W praktyce ta konstrukcja mówi: jeśli T pasuje do string, zwróć true; w przeciwnym razie zwróć false. W typach warunkowych ogromnie przydaje się też infer, bo pozwala nie tylko sprawdzić warunek, ale jeszcze wyciągnąć kawałek typu bez ręcznego „rozbierania” go na części.
type ElementType = T extends Array ? Item : T;
type Return = T extends (...args: never[]) => infer R ? R : never;
type A = ElementType; // string
type B = ElementType; // number
type C = Return<() => Promise>; // Promise To właśnie jest ten moment, w którym conditional types zaczynają robić realną robotę: nie tylko wybierają gałąź, ale też potrafią wydobyć informację z bardziej złożonego typu. Trzeba tylko pamiętać o jeszcze jednej rzeczy, która zaskakuje wiele osób pracujących już na średnim poziomie.
type ToArray = T extends any ? T[] : never;
type A = ToArray; // string[] | number[] Przy unii typów warunek rozdziela się na osobne przypadki, więc TypeScript analizuje każdy składnik po kolei. Jeśli chcesz to zachowanie wyłączyć, zwykle opakowuję typ w krotkę.
type ToArrayNonDistributive = [T] extends [any] ? T[] : never;
type A = ToArrayNonDistributive; // (string | number)[] To rozróżnienie robi dużą różnicę w bibliotekach i pomocniczych typach. Gdy już je widać, łatwiej ocenić, które wzorce są naprawdę użyteczne, a które tylko wyglądają sprytnie.
Wzorce, które naprawdę przydają się w projekcie
Najlepsze zastosowania conditional types są zwykle bardzo konkretne. Ja najczęściej widzę je tam, gdzie trzeba przefiltrować unię, wyciągnąć fragment z typu obiektu albo dopasować wynik funkcji do wejścia. Poniżej zestawiam kilka wzorców, które wracają najczęściej.
| Wzorzec | Co robi | Dlaczego się przydaje |
|---|---|---|
| Filtrowanie unii | Zostawia tylko pasujące typy | Pomaga usuwać przypadki niechciane bez ręcznego rozpisywania wariantów |
| Wyciąganie typu z tablicy | Tablica zamienia się w typ elementu | Ułatwia budowanie helperów do kolekcji i danych API |
| Odczyt typu zwracanego funkcji | Analizuje sygnaturę i zwraca wynik | Przydatne w wrapperach, callbackach i narzędziach ogólnych |
| Rozpakowywanie Promise | Wyciąga docelowy typ spod asynchronicznej otoczki | To podstawa helperów podobnych do Awaited
|
Filtracja unii to klasyczny przykład, bo pozwala zbudować typ, który zachowuje tylko pasujące składniki. Tak działa na przykład prosty helper do wyciągania samych stringów.
type OnlyStrings = T extends string ? T : never;
type A = OnlyStrings<"a" | 1 | "b" | false>; // "a" | "b" Ważne jest tutaj słowo never. W gałęzi fałszywej oznacza ono, że dany składnik unii znika z wyniku. To bardzo elegancki sposób filtrowania, ale tylko wtedy, gdy faktycznie chcesz usuwać przypadki niepasujące, a nie ukrywać błąd projektowy.
Drugim praktycznym wzorcem jest wyciąganie informacji z funkcji i struktur danych. Wbudowane utility types TypeScriptu, takie jak ReturnType czy Awaited, pokazują dokładnie, po co ten mechanizm istnieje: żeby nie przepisywać za każdym razem tej samej logiki opisującej typowy kształt danych. W dobrze napisanym helperze robi to mniej hałasu niż ręczne utrzymywanie kilku rozłącznych aliasów.
To prowadzi do następnego pytania, które zadaję sobie przy każdym nowym typie: czy to rozwiązanie naprawdę upraszcza kod, czy tylko sprawia, że wygląda bardziej zaawansowanie?
Kiedy warto ich użyć, a kiedy lepiej wybrać prostsze rozwiązanie
Conditional types nie są „lepsze” od innych narzędzi z definicji. Są dobre wtedy, gdy zależność między typami jest realna i powtarzalna. Jeśli zależność jest tylko przybliżona albo liczba gałęzi zaczyna rosnąć, często wygrywa prostszy model.
| Rozwiązanie | Kiedy wygrywa | Kiedy przegrywa |
|---|---|---|
| Typy warunkowe | Gdy typ wyniku zależy od typu wejściowego | Gdy logika robi się wielopoziomowa i trudna do śledzenia |
| Przeciążenia funkcji | Gdy publiczne API ma kilka czytelnych wariantów | Gdy trzeba opisać dużo kombinacji wejścia i wyjścia |
| Union types | Gdy wystarczą proste, jawne warianty | Gdy wynik musi zależeć od konkretnego wejścia |
| Mapped types | Gdy przetwarzasz pola obiektu | Gdy potrzebujesz decyzji typu „jeśli to, to tamto” |
Ja zwykle wybieram przeciążenia albo zwykły union wtedy, gdy chodzi o czytelność API dla zespołu. Conditional types zostawiam tam, gdzie pomagają zamknąć powtarzalną regułę w jednym miejscu. To dobra granica: jeśli po miesiącu nadal da się to przeczytać bez walki z nawiasami i zagnieżdżeniami, rozwiązanie prawdopodobnie jest trafione.
Jeżeli jednak zaczynasz budować złożony system zależności w samym typie, to często znak, że problem powinien zostać rozbity na mniejsze helpery albo przeniesiony do prostszego modelu danych. I właśnie wtedy pojawiają się pułapki, o których łatwo zapomnieć przy pierwszej, bardzo eleganckiej wersji kodu.
Najczęstsze pułapki i jak ich unikam
Najczęstszy błąd polega na tym, że typ warunkowy wygląda prosto, ale jego zachowanie na uniach już nie jest takie oczywiste. Druga pułapka to zbyt luźne użycie never albo zbyt ambitne zagnieżdżanie kilku warunków naraz. Wtedy kompilator dalej „działa”, tylko człowiek coraz mniej rozumie, co właściwie opisuje.
- Niespodziewana dystrybucja po unii - jeśli wynik ma być wspólny dla całej unii, a nie osobny dla każdego składnika, opakuj typ w krotkę.
-
Ukrywanie błędów przez
never- używaj go świadomie jako filtr, nie jako wygodny śmietnik na niepasujące przypadki. - Przeładowane zagnieżdżenia - dwa warunki są zwykle jeszcze do obrony, pięć warunków zaczyna być kosztowne w utrzymaniu.
- Oczekiwanie pełnej precyzji przy overloadach - przy typach funkcji z wieloma sygnaturami inference zwykle bierze ostatnią, najbardziej ogólną wersję.
W praktyce bardzo pomaga też szybkie sprawdzanie takich typów w TypeScript Playground albo w lokalnym pliku testowym. To nie jest efektowny krok, ale oszczędza sporo czasu, bo błędy w typach warunkowych często są logiczne, nie składniowe. Widać je dopiero wtedy, gdy przetestujesz kilka realnych przypadków, a nie tylko jeden idealny.
Jeśli zależy mi na wyłączeniu dystrybucji, stosuję prosty wzór z krotką. Jeśli zależy mi na filtrowaniu, świadomie zostawiam never. Ta dyscyplina robi większą różnicę niż sama „sprytność” typu.
Jak pisać je tak, żeby były czytelne po miesiącu
Ja najczęściej zaczynam od pytania: jaka reguła biznesowa albo techniczna naprawdę stoi za tym typem? Jeśli nie umiem odpowiedzieć jednym zdaniem, zwykle jeszcze nie mam gotowego helpera. Dobrze napisany typ warunkowy nie powinien wymagać od czytelnika rozgryzania intencji autora metodą prób i błędów.
- Nazwij typ po efekcie, nie po mechanice, na przykład
ElementTypezamiast ogólnegoConditional1. - Rozbij złożone przypadki na mniejsze aliasy, zamiast upychać wszystko w jednej długiej definicji.
- Zostaw prosty fallback, najczęściej
neveralbo oryginalny typ, żeby wynik był przewidywalny. - Sprawdź kilka reprezentatywnych przykładów, nie tylko ten, który masz akurat w głowie.
- Jeśli typ odzwierciedla dane z zewnątrz, połącz go z walidacją runtime, bo sam TypeScript nie zweryfikuje zawartości API.
W projektach webowych to połączenie typów i walidacji jest naprawdę ważne. Typ warunkowy może świetnie opisać formę danych, ale nie obroni Cię przed błędnym JSON-em z backendu albo źle zmapowanym payloadem. Dlatego traktuję conditional types jako warstwę porządkującą kod, a nie jako substytut kontroli danych.
Najlepsze efekty widzę w kodzie współdzielonym: helperach, bibliotekach UI, SDK, adapterach do API i narzędziach ogólnych. W zwykłym komponencie Reacta czy prostym module domenowym często lepiej wybrać czytelny union albo zwykłą funkcję niż budować z typów osobny język. Tam, gdzie zależność jest realna i powtarzalna, typy warunkowe dają dużą przewagę. Tam, gdzie problem jest prosty, prosty powinien zostać również typ.