Conditional Types: Typen basierend auf Bedingungen ableiten
<T>
type
TypeScript · Conditional Types · Generics · Utility Types
Conditional Types: Typen basierend auf Bedingungen ableiten
T extends U ? X : Y verstehen, nutzen und wann man sie meidet

Conditional Types erlauben es, den Typ eines Ausdrucks von einer Bedingung über einen anderen Typ abhängig zu machen. Sie stecken hinter fast jedem eingebauten Utility-Type wie Exclude, ReturnType oder Awaited und lösen Probleme, die sonst nur mit Type Assertions oder doppeltem Code lösbar wären. Dieser Artikel zeigt Syntax, Praxisfälle und die Grenze zur Lesbarkeit.

13 Min. Lesezeit T extends U ? X : Y · infer · Distributive Types TypeScript 5.x

1. Das Problem: Typen, die von anderen Typen abhängen

Ein statisches Typsystem beschreibt normalerweise eine feste Form: Ein Wert hat den Typ string, ein Objekt hat genau diese Felder. In der Praxis hängt der korrekte Typ eines Ausdrucks aber häufig von einem anderen Typ ab. Der Rückgabetyp einer Funktion hängt von ihrer Signatur ab, ein Feld soll nur existieren, wenn ein anderes Feld einen bestimmten Literalwert trägt, oder eine Union soll auf genau die Varianten reduziert werden, die zu einem konkreten Kontext passen. Ohne ein Sprachmittel dafür bleibt nur der Griff zu any, zu manuellen Type Assertions oder zu doppelt gepflegten Typdefinitionen, die bei jeder Änderung der Quelle auseinanderlaufen.

Conditional Types lösen genau dieses Problem: Sie erlauben es, einen Typ als Bedingung über einen anderen Typ auszudrücken, ausgewertet vom Compiler zur Kompilierzeit. Das ist ein fortgeschrittenes Feature, das die meisten Entwickler nicht täglich selbst schreiben, aber ständig indirekt nutzen. Jeder, der ReturnType<typeof fn>, Exclude<A, B> oder Awaited<Promise<T>> verwendet, verlässt sich bereits auf Conditional Types, die in der TypeScript-Standardbibliothek definiert sind. Wer versteht, wie sie funktionieren, kann Compiler-Fehlermeldungen besser lesen und die Typdefinitionen von Bibliotheken wie GraphQL-Codegen-Ausgaben oder ORM-Query-Buildern tatsächlich nachvollziehen, statt sie als Blackbox zu akzeptieren.

2. Grundlagen: Die Syntax T extends U ? X : Y

Ein Conditional Type hat die Form T extends U ? X : Y und ähnelt bewusst dem ternären Operator auf Werteebene, arbeitet aber vollständig auf Typebene. Ist T dem Typ U zuweisbar, löst der gesamte Ausdruck zu X auf, andernfalls zu Y. Das Schlüsselwort extends bedeutet hier nicht Klassenvererbung, sondern Zuweisbarkeit: T extends U prüft, ob jeder Wert vom Typ T auch ein gültiger Wert vom Typ U wäre. Diese Prüfung funktioniert für primitive Typen, Objektformen, Funktionssignaturen und Literale gleichermaßen.

Ein einfaches Beispiel macht den Mechanismus greifbar: type IsString<T> = T extends string ? true : false; liefert für konkrete Typen sofort ein Ergebnis. Interessanter wird es, sobald T ein generischer Typparameter ist, der erst bei der Verwendung mit einem konkreten Typ gefüllt wird. In diesem Fall wertet der Compiler den Conditional Type nicht sofort aus, sondern erst, wenn der Typparameter tatsächlich substituiert wurde. Dieses verzögerte Auswertungsverhalten ist die Grundlage für das im nächsten Abschnitt beschriebene distributive Verhalten über Union-Typen.


// Basic conditional type: mirrors a ternary operator, but operates on types
type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">; // true
type B = IsString<42>;      // false

// Conditional types compose well with indexed access types
type Flatten<T> = T extends unknown[] ? T[number] : T;

type Single = Flatten<string>;    // string, T is not an array, Y branch
type Element = Flatten<number[]>; // number, T is an array, X branch resolves the element type

3. Distributive Conditional Types über Union-Typen

Wird ein Conditional Type mit einem sogenannten nackten Typparameter (naked type parameter) geprüft, also direkt T extends U ? X : Y ohne T in ein Array, ein Tupel oder eine andere Hülle zu packen, und ist T zum Zeitpunkt der Instanziierung ein Union-Typ, dann verteilt TypeScript den Conditional Type automatisch über jedes Mitglied der Union einzeln. Das Ergebnis ist die Union der einzelnen Auswertungen, nicht eine einzige Auswertung über die gesamte Union. Dieses Verhalten heißt Distributive Conditional Type und ist eines der am häufigsten missverstandenen Details des TypeScript-Typsystems.

Praktisch bedeutet das: ToArray<string | number> wird nicht als eine einzige Prüfung über string | number ausgewertet, sondern als ToArray<string> | ToArray<number>, was zu string[] | number[] führt. Wer dieses Verteilungsverhalten bewusst verhindern will, etwa weil die Union als Ganzes geprüft werden soll, kapselt T in ein Tupel: [T] extends [U] ? X : Y. Ein Tupel mit genau einem Element ist kein nackter Typparameter mehr, wodurch die Distribution ausbleibt. Genau dieser Trick wird in einigen fortgeschrittenen Utility-Types eingesetzt, wenn eine Union nicht Mitglied für Mitglied, sondern als zusammenhängender Typ behandelt werden muss.


// Distributive conditional type: a naked (bare) type parameter distributes over a union
type ToArray<T> = T extends unknown ? T[] : never;

type Result = ToArray<string | number>;
// Distributes: ToArray<string> | ToArray<number> = string[] | number[]

// Wrapping the checked type in a tuple disables distribution
type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;

type NonDistResult = ToArrayNonDist<string | number>;
// The union is checked as a whole: (string | number)[]

// This distribution is exactly how NonNullable<T> works internally
type WithoutNullish<T> = T extends null | undefined ? never : T;
type CleanUnion = WithoutNullish<string | null | number | undefined>;
// string | number, each union member is checked and filtered individually

4. Praxis: Rückgabe- und Parametertypen extrahieren

Bevor ReturnType und Parameters fester Bestandteil der TypeScript-Standardbibliothek waren, mussten Entwickler diese Extraktionen selbst schreiben, und dasselbe Muster lohnt sich bis heute, sobald eine spezialisierte Variante gebraucht wird, etwa ein AsyncReturnType, das automatisch aus einer Promise entpackt. Die zugrunde liegende Technik ist immer dieselbe: Ein Conditional Type prüft, ob der übergebene Typ einer Funktionssignatur entspricht, und erfasst dabei mit infer den Teil der Signatur, der interessiert.

In der Praxis reduziert das Duplizierung erheblich. Statt eine DTO-Struktur manuell parallel zur Signatur einer Repository-Methode oder eines API-Client-Aufrufs zu pflegen, leitet man den passenden Typ direkt aus der Funktion ab. Ändert sich die Signatur, etwa weil ein Feld einer Bestellantwort ergänzt wird, zieht der abgeleitete Typ automatisch nach, ohne dass eine zweite Stelle im Code aktualisiert werden muss. Das ist besonders wertvoll für Test-Fixtures und Mocks, die exakt der Form entsprechen müssen, die eine echte Funktion zur Laufzeit liefert.


// Custom ReturnType implementation, the same technique the TS standard library uses
type MyReturnType<Fn extends (...args: never[]) => unknown> =
  Fn extends (...args: never[]) => infer Return ? Return : never;

// Custom Parameters implementation
type MyParameters<Fn extends (...args: never[]) => unknown> =
  Fn extends (...args: infer Args) => unknown ? Args : never;

declare function createOrder(customerId: string, items: string[]): { orderId: string };

type CreateOrderResult = MyReturnType<typeof createOrder>; // { orderId: string }
type CreateOrderArgs = MyParameters<typeof createOrder>;   // [customerId: string, items: string[]]

// Useful for deriving test fixtures without duplicating the function signature
const mockResult: CreateOrderResult = { orderId: "ORD-1001" };

5. Praxis: Union-Mitglieder gezielt filtern

Distributive Conditional Types eignen sich hervorragend, um eine breite Union auf genau die Varianten zu reduzieren, die in einem bestimmten Kontext relevant sind. Das Grundmuster ist immer dasselbe: T extends Pattern ? T : never behält ein Mitglied, wenn es auf das Muster passt, und T extends Pattern ? never : T entfernt es. Weil never in einer Union automatisch verschwindet, ergibt die Verteilung über alle Mitglieder am Ende genau die gewünschte Teilmenge, ganz ohne manuelle Filterung zur Laufzeit.

Ein realistisches Beispiel ist eine Discriminated Union von Events, etwa { kind: "created" } | { kind: "updated" } | { kind: "error"; message: string }. Mit type OnlyErrors<T> = T extends { kind: "error" } ? T : never; extrahiert man genau die Fehlervariante, während type WithoutErrors<T> = T extends { kind: "error" } ? never : T; sie herausfiltert. Dieses Muster ist die Grundlage von Extract und Exclude aus der Standardbibliothek und lohnt sich immer dann selbst zu schreiben, wenn die Standard-Utility-Types die gewünschte Bedingung nicht exakt abbilden, etwa bei einer Filterung nach mehreren Feldern gleichzeitig.

6. Das infer-Keyword: Typen aus der Struktur extrahieren

Das Schlüsselwort infer ist ausschließlich innerhalb der extends-Klausel eines Conditional Types erlaubt und deklariert eine neue Typvariable, die aus der Struktur des geprüften Typs erfasst wird. Statt nur zu fragen, ob T auf ein Muster passt, kann man mit infer zusätzlich einen Teil dieses Musters benennen und im X-Zweig weiterverwenden. Ohne infer müsste man diesen Teiltyp entweder schon vorher kennen oder ihn über eine separate Indexed-Access-Konstruktion umständlich herausschälen.

infer lässt sich rekursiv einsetzen, was besonders bei verschachtelten Strukturen wie mehrfach verschachtelten Promises nützlich ist. Taucht infer an mehreren Stellen desselben Musters auf, kombiniert TypeScript die erfassten Kandidaten je nach Position entweder zu einer Union oder zu einer Intersection, ein Detail, das für die meisten alltäglichen Anwendungsfälle wie Array-Elementtypen oder aufgelöste Promise-Werte keine Rolle spielt, aber bei sehr generischen Bibliothekstypen relevant werden kann.


// infer introduces a new type variable captured from a matched structure
type ElementType<T> = T extends (infer Item)[] ? Item : never;

type ProductId = ElementType<number[]>; // number

// infer works recursively when combined with itself
type UnwrapPromise<T> = T extends Promise<infer Value> ? UnwrapPromise<Value> : T;

type Resolved = UnwrapPromise<Promise<Promise<string>>>; // string

// Practical example: extracting the payload type from an async repository call
declare function fetchCustomer(id: string): Promise<{ id: string; email: string }>;

type FetchCustomerResult = UnwrapPromise<ReturnType<typeof fetchCustomer>>;
// { id: string; email: string }

7. Eingebaute Utility-Types: Exclude, Extract, ReturnType, Awaited

Fast alle Utility-Types, die in lib.es5.d.ts und den neueren lib.esnext.d.ts-Dateien der TypeScript-Standardbibliothek definiert sind, sind dünne Wrapper um genau die Techniken aus den vorherigen Abschnitten. Ihre tatsächlichen Definitionen zu kennen, entmystifiziert Compiler-Fehlermeldungen, die einen Conditional Type erwähnen, und hilft dabei, eigene Varianten für Spezialfälle zu bauen. type Exclude<T, U> = T extends U ? never : T; und type Extract<T, U> = T extends U ? T : never; sind exakt das Filtermuster aus Abschnitt 5, nur allgemein für beliebige Muster U statt für ein festes Objektmuster.

type ReturnType<T> = T extends (...args: any) => infer R ? R : any; entspricht der eigenen Implementierung aus Abschnitt 4, nur mit einer lockereren Funktionssignatur als Constraint. Awaited<T> geht noch einen Schritt weiter als das einfache UnwrapPromise-Beispiel aus Abschnitt 6, weil es zusätzlich beliebige Thenable-Objekte rekursiv über deren then-Methode auflöst, nicht nur echte Promise-Instanzen. Wer eine eigene Variante wie DeepAwaited für verschachtelte Objektstrukturen mit Promise-Feldern braucht, kombiniert diese Bausteine typischerweise mit Mapped Types, statt bei null anzufangen.

8. Wo Conditional Types die Lesbarkeit ruinieren

Conditional Types lassen sich verketten, und genau das verleitet dazu, Laufzeitlogik mit vielen Verzweigungen eins zu eins auf Typebene nachzubilden. Eine Kette aus fünf oder sechs verschachtelten Ternary-Ausdrücken, kombiniert mit mehreren infer-Deklarationen, ist für den Compiler kein Problem, aber für jeden Menschen, der den Typ später liest oder debuggt, eine Zumutung. Compiler-Fehlermeldungen zu solchen Konstrukten werden schnell zu Textwänden, die die eigentliche Ursache eines Typfehlers verschleiern statt aufzuklären, und jede zusätzliche Verzweigung erschwert das Nachvollziehen exponentiell statt linear.

Der bessere Weg bei mehr als zwei oder drei Verzweigungen ist fast immer ein benannter Hilfstyp: eine Lookup-Map, ein Discriminated Union nach dem Muster aus Abschnitt 5, oder bei funktionsbezogener Logik ein Satz von Funktions-Overloads. Conditional Types entfalten ihre Stärke bei eng begrenzten, klar benannten Transformationen wie Extraktion oder Filterung, nicht als generische Programmiersprache auf Typebene. Wer eine Typdefinition öfter erklären muss, als sie zu verwenden, hat in der Regel die falsche Abstraktion gewählt.


// Hard to read: deeply nested conditional chain emulating if/else if/else
type CssUnit<T> =
  T extends "px" ? number :
  T extends "%" ? `${number}%` :
  T extends "rem" ? `${number}rem` :
  T extends "vh" ? `${number}vh` :
  T extends "vw" ? `${number}vw` :
  never;

// Simpler and easier to extend: a lookup map instead of a growing ternary chain
interface CssUnitMap {
  px: number;
  "%": `${number}%`;
  rem: `${number}rem`;
  vh: `${number}vh`;
  vw: `${number}vw`;
}

type CssUnitLookup<T extends keyof CssUnitMap> = CssUnitMap[T];
// Same result, but adding a new unit means adding one line to an object,
// not inserting another branch into a growing ternary chain

9. Conditional Types im direkten Vergleich

Nicht jede Bedingung auf Typebene gehört in einen Conditional Type. Die folgende Übersicht zeigt typische Szenarien, bei denen ein Conditional Type zwar funktioniert, eine Alternative aber langfristig wartbarer ist.

Szenario Unübersichtlich / riskant Empfohlene Alternative Vorteil
Mehr als 3 Verzweigungen T extends A ? .. : T extends B ? .. : .. Lookup-Map / benannter Hilfstyp Neue Fälle sind eine Zeile, keine neue Verzweigung
Typ aus Funktionssignatur Struktur manuell dupliziert gepflegt ReturnType / infer-basierte Extraktion Bleibt automatisch mit der Signatur synchron
Verzweigung nach Laufzeitwert Cleverer Conditional Type über Literal-Union Discriminated Union + switch Narrowing funktioniert auch für Menschen, nicht nur den Compiler
Tief rekursiver Conditional Type Unbegrenzte Rekursion ohne Tiefenlimit Rekursion mit Zähler/Abbruchbedingung kappen Verhindert "excessively deep" Compiler-Fehler
Union auf Kontext filtern Filterung zur Laufzeit mit manuellen Type Guards Extract / Exclude-Muster Filterung passiert bereits beim Kompilieren

Die Faustregel aus der Tabelle lässt sich verallgemeinern: Ein Conditional Type ist die richtige Wahl, wenn er eine einzelne, klar benannte Transformation kapselt, etwa Extrahieren, Filtern oder Entpacken. Sobald ein Typ beginnt, mehrschichtige Geschäftslogik nachzubilden, ist ein Discriminated Union mit echtem Narrowing im Anwendungscode fast immer die robustere und leichter erklärbare Lösung, selbst wenn der Conditional Type technisch dasselbe Ergebnis liefern könnte.

Mironsoft

TypeScript-Tooling, Frontend-Architektur und typsichere Headless-Integrationen

Conditional Types, die euer Team auch in einem Jahr noch versteht?

Wir prüfen bestehende Typdefinitionen auf überkomplexe Conditional-Type-Ketten, ersetzen sie gezielt durch lesbare Alternativen und bauen dort, wo es sich lohnt, saubere Utility-Types für euren Magento- und Headless-Stack.

Type-Level-Review

Conditional Types und Utility-Types auf Lesbarkeit und Wartbarkeit prüfen

API-Typisierung

Typen direkt aus Funktionssignaturen und GraphQL-Schemas ableiten

Build-Tooling

Strikte tsconfig-Einstellungen und Typ-Checks in der CI-Pipeline

10. Zusammenfassung

Conditional Types lösen ein konkretes Problem: Typen, die von der Form eines anderen Typs abhängen, statt starr fixiert zu sein. Die Syntax T extends U ? X : Y verhält sich wie ein Ternary-Operator auf Typebene, verteilt sich bei nackten Typparametern automatisch über Union-Typen und lässt sich mit infer um das Erfassen von Teiltypen erweitern. Genau diese Bausteine stecken hinter Exclude, Extract, ReturnType und Awaited, den am häufigsten verwendeten Utility-Types der TypeScript-Standardbibliothek.

Der entscheidende Punkt ist die Grenze zur Lesbarkeit. Conditional Types sind ideal für eng begrenzte Transformationen wie Extrahieren, Entpacken oder Filtern von Typen. Sobald eine Kette von Verzweigungen beginnt, Laufzeitlogik eins zu eins auf Typebene nachzubilden, wird ein benannter Hilfstyp, eine Lookup-Map oder ein Discriminated Union fast immer zur besseren Wahl, weil er sowohl für den Compiler als auch für das nächste Teammitglied verständlich bleibt.

Conditional Types in TypeScript - Das Wichtigste auf einen Blick

Grundsyntax

T extends U ? X : Y verhält sich wie ein Ternary-Operator, aber vollständig auf Typebene ausgewertet.

Distributive Verhalten

Nackte Typparameter verteilen den Conditional Type über jedes Union-Mitglied. [T] extends [U] deaktiviert das.

infer-Keyword

Erfasst einen Teiltyp direkt aus der Struktur, Grundlage von ReturnType, Parameters und Awaited.

Lesbarkeitsgrenze

Ab drei oder mehr Verzweigungen: Lookup-Map, Discriminated Union oder benannter Hilfstyp statt weiterer Verkettung.

11. FAQ: Conditional Types in TypeScript

1Was ist ein Conditional Type in TypeScript?
Ein Typ, der von einer Bedingung über einen anderen Typ abhängt, ausgedrückt als T extends U ? X : Y. Der Compiler wertet die Bedingung zur Kompilierzeit aus.
2Wie funktioniert die Syntax T extends U ? X : Y genau?
extends prüft Zuweisbarkeit, nicht Vererbung. Ist T einem Wert von U zuweisbar, löst der Ausdruck zu X auf, sonst zu Y. Bei generischen Parametern wird bis zur Substitution verzögert.
3Was bedeutet distributiv bei Conditional Types?
Bei einem nackten Typparameter mit Union-Typ wertet TypeScript den Conditional Type für jedes Mitglied einzeln aus und vereint die Ergebnisse.
4Wie verhindere ich, dass ein Conditional Type über eine Union verteilt wird?
Den geprüften Typ in ein Ein-Element-Tupel packen: [T] extends [U] ? X : Y. Das deaktiviert die automatische Verteilung.
5Was macht das infer-Keyword?
Deklariert innerhalb der extends-Klausel eine neue Typvariable, die einen Teil der geprüften Struktur erfasst und im X-Zweig weiterverwendet wird.
6Wie sind ReturnType und Parameters intern implementiert?
Beide sind Conditional Types mit infer: ReturnType erfasst den Rückgabetyp mit infer R, Parameters die Argumentliste als Tupel mit infer Args.
7Was ist der Unterschied zwischen Exclude und Extract?
Exclude entfernt Union-Mitglieder, die auf U passen. Extract behält nur die passenden. Beide beruhen auf demselben distributiven Filtermuster, nur mit vertauschten Zweigen.
8Warum bekomme ich Type instantiation is excessively deep and possibly infinite?
Meist bei rekursiven Conditional Types ohne klare Abbruchbedingung oder zu hoher Rekursionstiefe. Eine explizite Zählvariable oder ein früher Abbruchfall behebt das üblicherweise.
9Wann sollte ich statt eines Conditional Type lieber eine Discriminated Union verwenden?
Ab mehr als zwei oder drei Verzweigungen oder wenn Laufzeitlogik nachgebildet wird. Eine Discriminated Union mit switch ist dann für Menschen leichter nachvollziehbar.
10Kann ich Conditional Types mit generischen Funktionen kombinieren?
Ja. Ein generischer Funktionsparameter kann in einen Conditional Type einfließen, sodass der Rückgabetyp automatisch von der Form des übergebenen Arguments abhängt.