Ein Conditional Type wählt zwischen zwei Typen mit einem Test, der wie der ternäre Operator von JavaScript aussieht: T extends U ? X : Y. Ist T an U zuweisbar, ist das Ergebnis X, sonst Y.
extends heißt hier "ist zuweisbar an", dieselbe Beziehung, die der Compiler nutzt, wenn du einer Variablen einen Wert zuweist. Conditional Types existieren nur beim Kompilieren; aus der JavaScript-Ausgabe werden sie entfernt.
Die Syntax
type Result = CheckedType extends TestType ? TrueType : FalseType;
Nützlich werden Conditional Types mit Generics, wo der geprüfte Typ ein Typparameter ist, der später einen konkreten Typ bekommt. Im true-Zweig weiß TypeScript, dass der geprüfte Typ zum Test passt, also ist T["message"] oben erlaubt, obwohl ein einfaches T keine Eigenschaft message hat.
Sie lassen sich auch verschachteln, wie verkettete ternäre Operatoren:
Das as TypeName<T> im Funktionsrumpf ist nicht optional, wie der nächste Abschnitt zeigt.
Bedingte Rückgabetypen brauchen eine Assertion
Eine Funktion, deren Rückgabetyp ein Conditional Type über ihren eigenen Typparameter ist, kann keinen der beiden Zweige direkt zurückgeben. TypeScript grenzt T im Rumpf nicht ein, kann also nicht sagen, welcher Zweig gilt:
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>'.
Zwei übliche Lösungen: Overloads, die jedes Paar aus Eingabe und Ausgabe angeben und Aufrufer genau prüfen, oder eine Assertion in der Implementierung.
Overloads behandelt die Seite zum Überladen von Funktionen. Bei einer Assertion vertraut dir der Compiler: Ein falscher Zweig im Rumpf würde nicht auffallen.
Distributive Conditional Types
Ist der geprüfte Typ ein nackter Typparameter und bekommt er eine Union, läuft die Bedingung einmal pro Mitglied, und die Ergebnisse werden zu einer neuen Union vereinigt:
Die Distribution ist das, was Exclude und Extract funktionieren lässt. Exclude<T, U> ist als T extends U ? never : T definiert: Jedes Mitglied, das zu U passt, wird zu never, und never verschwindet aus einer Union. Also ist Exclude<"a" | "b" | "c", "a"> gleich "b" | "c".
Aus derselben Regel folgen zwei Überraschungen. boolean ist die Union true | false, also ist ToArray<boolean> gleich false[] | true[], nicht boolean[]. Und never ist die leere Union, also gibt ein distributiver Conditional Type bei never einfach never zurück, ohne etwas zu testen:
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
Typen mit infer herausziehen
infer deklariert eine neue Typvariable in der extends-Klausel. Passt der Test, füllt TypeScript diese Variable aus dem geprüften Typ, und du kannst sie im true-Zweig verwenden:
Lies T extends Promise<infer V> ? V : T als "wenn T ein Promise von irgendetwas ist, nenne dieses Etwas V und gib es zurück; sonst gib T unverändert zurück". infer ist nur in der extends-Klausel eines Conditional Types erlaubt.
Eine infer-Variable kann mit extends einen eigenen Constraint tragen. Dann passt der Test nur, wenn der abgeleitete Typ dazu passt:
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
ReturnType selbst bauen
Das eingebaute ReturnType ist ein einzeiliger Conditional Type mit infer. Es selbst zu schreiben, ist die klassische Übung, bei der beide Ideen klick machen:
typeof makeUser macht aus dem Funktionswert seinen Typ, dann vergleicht der Conditional Type ihn mit "irgendeiner Funktion" und hält den Rückgabetyp als R fest. Die Version der Standardbibliothek unterscheidet sich in zwei Details: Ihr Parameter ist auf Funktionstypen beschränkt (T extends (...args: any) => any), also ist ReturnType<string> ein Compilerfehler statt never, und ihr false-Zweig ist any. Die ReturnType-Seite behandelt auch Parameters, InstanceType und Awaited.
Rekursive Conditional Types
Ein Conditional Type kann sich auf sich selbst beziehen und so jede Verschachtelungstiefe auspacken:
type Flatten<T> = T extends readonly (infer U)[] ? Flatten<U> : T;
type A = Flatten<number[][][]>; // number
type B = Flatten<string>; // string
Das eingebaute Awaited<T> funktioniert so und packt Promise<Promise<T>> bis auf T aus. Halte die Rekursion in der Praxis flach: Bei sehr tiefer oder unbegrenzter Rekursion gibt der Compiler auf mit error TS2589: Type instantiation is excessively deep and possibly infinite.
Kurzreferenz
| Muster | Bedeutung |
|---|---|
T extends U ? X : Y | X, wenn T an U zuweisbar ist, sonst Y |
T extends U ? never : T | Mitglieder entfernen, die zu U passen (das ist Exclude) |
T extends U ? T : never | Mitglieder behalten, die zu U passen (das ist Extract) |
[T] extends [U] ? X : Y | derselbe Test, ohne Verteilung über eine Union |
T extends (infer E)[] ? E : T | Elementtyp eines Arrays |
T extends Promise<infer V> ? V : T | Werttyp eines Promise |
T extends (...args: any[]) => infer R ? R : never | Rückgabetyp einer Funktion |
T extends [infer H, ...infer Rest] ? ... | erstes Element und Rest eines Tupels |
Häufig gestellte Fragen
Was ist ein Conditional Type in TypeScript?
Ein Typ der Form T extends U ? X : Y. Ist T an U zuweisbar, ist das Ergebnis X, sonst Y. Es ist ein if/else für Typen, ausgewertet beim Kompilieren; im erzeugten JavaScript bleibt nichts davon übrig.
Was macht das Schlüsselwort infer in TypeScript?
infer deklariert eine Typvariable in der extends-Klausel eines Conditional Types und lässt TypeScript sie aus dem passenden Typ füllen. T extends Promise<infer V> ? V : T zieht den aufgelösten Werttyp aus einem Promise-Typ heraus. Verwenden lässt es sich nur in der extends-Klausel eines Conditional Types.
Was ist ein distributiver Conditional Type?
Wenn der geprüfte Typ ein nackter Typparameter ist und du eine Union übergibst, wird die Bedingung auf jedes Mitglied einzeln angewendet, und die Ergebnisse werden vereinigt. ToArray<string | number> mit type ToArray<T> = T extends unknown ? T[] : never wird zu string[] | number[]. Setze beide Seiten in eckige Klammern, [T] extends [unknown], um das abzuschalten.
Wie bekomme ich den Rückgabetyp einer Funktion in TypeScript?
Mit dem eingebauten ReturnType<typeof fn>. Es ist ein Conditional Type mit infer: T extends (...args: any) => infer R ? R : any. Bei async-Funktionen umschließt du es mit Awaited<...>, um den aufgelösten Wert statt des Promise zu bekommen.
Warum gibt IsNever<never> never statt true zurück?
never ist die leere Union, und ein distributiver Conditional Type läuft über die Mitglieder einer Union. Ohne Mitglieder gibt es nichts zu verarbeiten, und das Ergebnis ist never. Schreibe [T] extends [never] ? true : false, um auf never selbst zu testen.