Menu

Typy warunkowe i infer w TypeScript z przykładami

Typ warunkowy wybiera jeden z dwóch typów na podstawie testu: T extends U ? X : Y. Poznaj składnię, rozdzielanie typów warunkowych po uniach (i jak je wyłączyć), wyciąganie typów przez infer i samodzielne napisanie ReturnType.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Typ warunkowy wybiera między dwoma typami za pomocą testu, który wygląda jak operator trójargumentowy z JavaScriptu: T extends U ? X : Y. Jeśli T da się przypisać do U, wynikiem jest X; w przeciwnym razie Y.

extends oznacza tu „da się przypisać do”, czyli tę samą relację, której kompilator używa, gdy przypisujesz wartość do zmiennej. Typy warunkowe istnieją tylko w czasie kompilacji; znikają z wynikowego JavaScriptu.

Składnia

type Result = CheckedType extends TestType ? TrueType : FalseType;

Typy warunkowe stają się przydatne razem z typami generycznymi, gdzie sprawdzany typ to parametr typu, który później dostaje konkretny typ. W gałęzi true TypeScript wie, że sprawdzany typ pasuje do testu, więc T["message"] powyżej jest dozwolone, choć zwykłe T nie ma właściwości message.

Można je też zagnieżdżać, jak łańcuch operatorów trójargumentowych:

as TypeName<T> w ciele funkcji nie jest opcjonalne, co pokazuje następna sekcja.

Warunkowe typy zwracane wymagają asercji

Funkcja, której typ zwracany to typ warunkowy zależny od jej własnego parametru typu, nie może bezpośrednio zwrócić żadnej z gałęzi. TypeScript nie zawęża T wewnątrz ciała, więc nie wie, która gałąź obowiązuje:

index.ts(5,34): error TS2322: Type 'number' is not assignable to type 'Flip<T>'.
index.ts(5,45): error TS2322: Type 'string' is not assignable to type 'Flip<T>'.

Dwa popularne rozwiązania: przeciążenia, które opisują każdą parę wejście i wyjście oraz precyzyjnie sprawdzają wywołujących, albo asercja w implementacji.

Przeciążenia opisuje strona o przeciążaniu funkcji. Przy asercji kompilator ci ufa: zła gałąź w ciele nie zostałaby wyłapana.

Rozdzielające typy warunkowe

Gdy sprawdzany typ to goły parametr typu i dostaje on unię, warunek wykonuje się raz dla każdego członu, a wyniki są łączone w nową unię:

Właśnie rozdzielanie sprawia, że działają Exclude i Extract. Exclude<T, U> jest zdefiniowane jako T extends U ? never : T: każdy człon pasujący do U staje się never, a never znika z unii. Dlatego Exclude<"a" | "b" | "c", "a"> to "b" | "c".

Z tej samej reguły wynikają dwie niespodzianki. boolean to unia true | false, więc ToArray<boolean> to false[] | true[], a nie boolean[]. Poza tym never to pusta unia, więc rozdzielający typ warunkowy, który dostaje never, zwraca never bez żadnego testu:

type IsNever<T> = T extends never ? true : false;
type X = IsNever<never>; // never, not true

type IsNeverFixed<T> = [T] extends [never] ? true : false;
type Y = IsNeverFixed<never>; // true

Wyciąganie typów przez infer

infer deklaruje nową zmienną typu wewnątrz klauzuli extends. Jeśli dopasowanie się uda, TypeScript wypełnia tę zmienną na podstawie sprawdzanego typu i możesz jej użyć w gałęzi true:

Czytaj T extends Promise<infer V> ? V : T jako „jeśli T jest obietnicą czegoś, nazwij to coś V i je zwróć; w przeciwnym razie zwróć T bez zmian”. infer jest dozwolone tylko w klauzuli extends typu warunkowego.

Zmienna infer może mieć własne ograniczenie przez extends. Dopasowanie udaje się wtedy tylko, gdy wywnioskowany typ pasuje:

type FirstString<T> = T extends [infer S extends string, ...unknown[]] ? S : never;

type A = FirstString<["a", 1]>; // "a"
type B = FirstString<[1, "a"]>; // never: the first element is not a string

Własna implementacja ReturnType

Wbudowany ReturnType to jednolinijkowy typ warunkowy z infer. Samodzielne napisanie go to klasyczne ćwiczenie, po którym obie idee stają się jasne:

typeof makeUser zamienia wartość funkcji w jej typ, a potem typ warunkowy dopasowuje go do „dowolnej funkcji” i przechwytuje typ zwracany jako R. Wersja z biblioteki standardowej różni się dwoma szczegółami: jej parametr jest ograniczony do typów funkcji (T extends (...args: any) => any), więc ReturnType<string> to błąd kompilacji, a nie never, a jej gałąź fałszywa to any. Strona o ReturnType omawia też Parameters, InstanceType i Awaited.

Rekurencyjne typy warunkowe

Typ warunkowy może odwoływać się do samego siebie, co pozwala rozpakować zagnieżdżenie dowolnej głębokości:

type Flatten<T> = T extends readonly (infer U)[] ? Flatten<U> : T;

type A = Flatten<number[][][]>; // number
type B = Flatten<string>;       // string

Wbudowany Awaited<T> działa w ten sposób, rozpakowując Promise<Promise<T>> aż do T. W praktyce trzymaj rekurencję płytką: bardzo głęboka albo nieograniczona rekurencja sprawia, że kompilator się poddaje z error TS2589: Type instantiation is excessively deep and possibly infinite.

Ściąga

WzorzecZnaczenie
T extends U ? X : YX, jeśli T da się przypisać do U, w przeciwnym razie Y
T extends U ? never : Tusuwa człony pasujące do U (to jest Exclude)
T extends U ? T : neverzostawia człony pasujące do U (to jest Extract)
[T] extends [U] ? X : Yten sam test, bez rozdzielania po unii
T extends (infer E)[] ? E : Ttyp elementu tablicy
T extends Promise<infer V> ? V : Ttyp wartości obietnicy
T extends (...args: any[]) => infer R ? R : nevertyp zwracany funkcji
T extends [infer H, ...infer Rest] ? ...pierwszy element i reszta krotki

Najczęściej zadawane pytania

Czym jest typ warunkowy w TypeScript?

To typ w postaci T extends U ? X : Y. Jeśli T da się przypisać do U, wynikiem jest X, w przeciwnym razie Y. To if/else dla typów, obliczane w czasie kompilacji; nic z niego nie istnieje w wyemitowanym JavaScripcie.

Co robi słowo kluczowe infer w TypeScript?

infer deklaruje zmienną typu wewnątrz klauzuli extends typu warunkowego i pozwala TypeScriptowi wypełnić ją na podstawie dopasowanego typu. T extends Promise<infer V> ? V : T wyciąga z typu obietnicy typ wartości, którą ona zwraca. Można go używać tylko w klauzuli extends typu warunkowego.

Czym jest rozdzielający typ warunkowy?

Gdy sprawdzany typ to goły parametr typu, a przekażesz unię, warunek zostaje zastosowany do każdego jej członu osobno, a wyniki są łączone. ToArray<string | number> przy type ToArray<T> = T extends unknown ? T[] : never staje się string[] | number[]. Opakuj obie strony w nawiasy kwadratowe, [T] extends [unknown], żeby to wyłączyć.

Jak uzyskać typ zwracany funkcji w TypeScript?

Użyj wbudowanego ReturnType<typeof fn>. To typ warunkowy z infer: T extends (...args: any) => infer R ? R : any. Dla funkcji async opakuj go w Awaited<...>, żeby dostać rozwiązaną wartość zamiast obietnicy.

Dlaczego IsNever<never> zwraca never zamiast true?

never to pusta unia, a rozdzielający typ warunkowy przechodzi po członach unii. Bez członów nie ma po czym przechodzić, więc wynikiem jest never. Napisz [T] extends [never] ? true : false, żeby sprawdzić samo never.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ