warum Exclude<A | B, A> funktioniert und wann Distribution stört
Wendet man einen Conditional Type auf einen Union-Typ an, wird die Bedingung standardmäßig auf jedes Mitglied der Union einzeln angewendet, nicht auf die Union als Ganzes. Dieses Verhalten heißt Distribution und ist die stille Grundlage von Utility-Typen wie Exclude und Extract. Wer die Naked-Type-Parameter-Regel nicht kennt, stolpert über unerwartete Ergebnisse, sobald ein eigener Utility-Typ Distribution bewusst verhindern soll.
Inhaltsverzeichnis
- 1. Was distributive Conditional Types sind
- 2. Die Regel: Naked Type Parameter für Distribution erforderlich
- 3. Praxisbeispiel: Die Mechanik von Exclude und Extract im Detail
- 4. Distribution verhindern mit Tupel-Wrapping
- 5. Nebenwirkung: Distribution über Union bei never
- 6. Praxisbeispiel: Eigene Utility-Typen mit bewusster Kontrolle
- 7. Interaktion mit Generic Constraints und Überladungen
- 8. Debugging: Wenn Distribution unerwartet zuschlägt
- 9. Distributive vs. Non-Distributive im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was distributive Conditional Types sind
Ein Conditional Type der Form T extends U ? X : Y verhält sich anders, sobald T zur Instanziierungszeit ein Union-Typ ist. Statt die gesamte Union in einem Schritt gegen U zu prüfen, wendet TypeScript die Bedingung auf jedes einzelne Mitglied der Union separat an und fügt die Ergebnisse anschließend wieder zu einer neuen Union zusammen. Dieses Verhalten heißt Distribution, weil sich der Conditional Type über die Union verteilt, statt sie als einzelnen, unteilbaren Typ zu behandeln.
Distributive Conditional Types sind kein Sonderfall, sondern das Standardverhalten jedes Conditional Types, sofern eine bestimmte syntaktische Bedingung erfüllt ist, die im nächsten Abschnitt erklärt wird. Dieses Verhalten ist die stille, oft unbemerkte Grundlage vieler eingebauter Utility-Typen, allen voran Exclude<T, U> und Extract<T, U>, die ohne Distribution schlicht nicht funktionieren würden.
Dieser Artikel erklärt die genaue Regel, wann Distribution eintritt, zeigt die Mechanik an Exclude und Extract im Detail, demonstriert, wie man Distribution gezielt verhindert, wenn sie unerwünscht ist, und benennt die bekannteste Nebenwirkung rund um den Typ never.
2. Die Regel: Naked Type Parameter für Distribution erforderlich
Distribution tritt nur dann ein, wenn der geprüfte Typparameter im Conditional Type als sogenannter Naked Type Parameter erscheint, also unverpackt und direkt, ohne ihn in ein Array, Tupel, Objekt oder eine andere Struktur einzubetten. T extends U ? X : Y distribuiert, weil T direkt und unverpackt in der Bedingung steht. [T] extends [U] ? X : Y distribuiert dagegen nicht, weil T hier innerhalb eines Tupels verpackt ist.
Diese Regel gilt unabhängig davon, ob T tatsächlich eine Union ist oder nicht. Der Compiler prüft nur die syntaktische Form der Definition, nicht die konkrete Instanziierung. Ein Conditional Type mit nacktem Typparameter distribuiert also immer über Unions, sobald er mit einer Union instanziiert wird, ein Conditional Type mit verpacktem Typparameter niemals, selbst wenn ebenfalls eine Union übergeben wird.
// Naked type parameter: distributes over union members automatically
type ToArrayNaked<T> = T extends unknown ? T[] : never;
type A = ToArrayNaked<string | number>;
// string[] | number[] — distribution happened
// Wrapped type parameter: does NOT distribute, union is treated as one type
type ToArrayWrapped<T> = [T] extends [unknown] ? T[] : never;
type B = ToArrayWrapped<string | number>;
// (string | number)[] — no distribution, union stays intact
3. Praxisbeispiel: Die Mechanik von Exclude und Extract im Detail
Die eingebauten Utility-Typen Exclude<T, U> und Extract<T, U> sind das anschaulichste Beispiel für distributive Conditional Types in der TypeScript-Standardbibliothek. Exclude<T, U> ist definiert als T extends U ? never : T, mit nacktem T als geprüftem Parameter. Wird T mit einer Union instanziiert, prüft TypeScript jedes Mitglied einzeln: passt ein Mitglied zu U, wird es durch never ersetzt, was in einer Union effektiv verschwindet.
Extract<T, U> funktioniert spiegelbildlich mit T extends U ? T : never: Mitglieder, die zu U passen, bleiben erhalten, alle anderen werden zu never und verschwinden. Ohne Distribution müssten beide Utility-Typen die Union als Ganzes gegen U prüfen, was bei Exclude<"a" | "b" | "c", "a"> entweder die komplette Union verwerfen oder komplett behalten würde, statt selektiv einzelne Mitglieder zu entfernen.
type Exclude2<T, U> = T extends U ? never : T; // naked T -> distributes
type Status = "pending" | "active" | "archived" | "deleted";
// Distribution checks each member of Status individually against "archived" | "deleted"
type ActiveStatus = Exclude2<Status, "archived" | "deleted">;
// "pending" | "active"
type Extract2<T, U> = T extends U ? T : never;
type ArchivedOrDeleted = Extract2<Status, "archived" | "deleted">;
// "archived" | "deleted"
4. Distribution verhindern mit Tupel-Wrapping
Manchmal ist Distribution unerwünscht, etwa wenn ein Conditional Type prüfen soll, ob eine Union als Ganzes einem bestimmten Typ entspricht, statt jedes Mitglied einzeln zu behandeln. Ein typisches Beispiel ist ein Typ, der prüft, ob T exakt string | number ist. Mit einem nackten Typparameter würde Distribution das Ergebnis für jede Kombination von Mitgliedern getrennt berechnen, was hier nicht das gewünschte Verhalten ist.
Das Tupel-Wrapping-Muster [T] extends [U] ? X : Y ist die Standardlösung, um Distribution gezielt zu unterdrücken. Weil T hier innerhalb eines Tupels steht, gilt die Naked-Type-Parameter-Regel aus Abschnitt 2 nicht, und der Compiler behandelt die gesamte Union als einen unteilbaren Typ, gegen den einmalig geprüft wird.
// Without wrapping: distributes, checks each union member separately
type IsStringOrNumberNaked<T> = T extends string | number ? true : false;
type A = IsStringOrNumberNaked<string | number>;
// true | false — distributed per member, not the intended result
// With wrapping: treats the union as a single, non-distributed type
type IsStringOrNumberWrapped<T> = [T] extends [string | number] ? true : false;
type B = IsStringOrNumberWrapped<string | number>;
// true — the union as a whole is checked once
5. Nebenwirkung: Distribution über Union bei never
Eine der am wenigsten intuitiven Nebenwirkungen von distributive Conditional Types betrifft den Typ never. Da never als leere Union verstanden werden kann, mit null Mitgliedern, führt Distribution über never dazu, dass der gesamte Conditional Type sofort zu never auswertet, unabhängig davon, was in den Zweigen X und Y steht. Der Compiler verteilt die Bedingung über null Mitglieder und erhält folgerichtig eine leere Ergebnis-Union.
Dieses Verhalten überrascht besonders bei generischen Funktionen, die mit einem potenziell never-wertigen Typparameter aufgerufen werden, etwa nach einer vorherigen Exclude-Operation, die versehentlich alle Mitglieder entfernt hat. Ein Conditional Type, der bei never ein bestimmtes Fallback-Verhalten erwartet, erhält stattdessen immer never als Ergebnis, was zu schwer nachvollziehbaren Folgefehlern führen kann.
type CheckNaked<T> = T extends string ? "yes" : "no";
type Result1 = CheckNaked<never>;
// never — NOT "no", because distribution over an empty union yields never
type CheckWrapped<T> = [T] extends [string] ? "yes" : "no";
type Result2 = CheckWrapped<never>;
// "no" — wrapping suppresses distribution, never is treated as a real type
6. Praxisbeispiel: Eigene Utility-Typen mit bewusster Kontrolle
Beim Schreiben eigener Utility-Typen sollte man bei jedem Conditional Type bewusst entscheiden, ob Distribution gewünscht ist, statt sie als Zufallsprodukt der Syntax zu akzeptieren. Ein NonNullableDistributed<T>, der null und undefined aus jedem Mitglied einer Union entfernt, profitiert von Distribution und sollte einen nackten Typparameter verwenden. Ein IsUnion<T>, der prüft, ob T überhaupt aus mehreren Mitgliedern besteht, muss dagegen Distribution unterdrücken, sonst würde die Prüfung selbst durch die Distribution ausgehebelt.
Ein bewährtes Muster für IsUnion<T> nutzt genau diesen Kontrast: Ein nackter Typparameter wird gegen denselben, aber künstlich eingeschränkten Typparameter geprüft. Bei einer echten Union führt die Distribution dazu, dass die Prüfung fehlschlägt, weil ein einzelnes Mitglied nie die volle Union umfasst, während ein einzelner Typ die Prüfung immer besteht.
// Deliberately distributive: strips null/undefined from every union member
type NonNullableDistributed<T> = T extends null | undefined ? never : T;
// Deliberately non-distributive: checks whether T is a union at all
type IsUnion<T, Copy = T> = T extends Copy
? [Copy] extends [T] ? false : true // if a single member covers the whole union, it's not a union
: never;
type A = IsUnion<string | number>; // true
type B = IsUnion<string>; // false
7. Interaktion mit Generic Constraints und Überladungen
Distribution interagiert auch mit Generic Constraints auf subtile Weise. Ein generischer Parameter T extends SomeUnion in einer Funktionssignatur führt dazu, dass jeder interne Conditional Type mit nacktem T distribuiert, sobald die Funktion mit einer konkreten Union instanziiert wird, selbst wenn der Constraint selbst nur eine Obermenge zulässiger Werte beschreibt. Entwickler, die eine Funktion mit einem einzelnen konkreten Typ aufrufen, sehen dabei kein Distributionsverhalten, weil bei einem einzelnen Typ Distribution und Nicht-Distribution zum selben Ergebnis führen.
Bei überladenen Funktionen, die für unterschiedliche Union-Mitglieder unterschiedliche Rückgabetypen liefern sollen, ist Distribution häufig genau das gewünschte Verhalten, weil TypeScript so für jedes Mitglied einzeln den passenden Rückgabetyp berechnet und am Ende zu einer präzisen Union zusammenführt, statt eine grobe Vereinigung aller möglichen Rückgabetypen zu erzwingen.
8. Debugging: Wenn Distribution unerwartet zuschlägt
Der häufigste Debugging-Fall bei distributive Conditional Types ist ein Ergebnistyp, der eine Union statt des erwarteten einzelnen Typs enthält, weil ein Conditional Type unbeabsichtigt über eine Union distribuiert hat. Der erste Diagnoseschritt ist immer, den Typparameter im Conditional Type zu prüfen: Steht er nackt in der Bedingung, ist Distribution die wahrscheinliche Ursache für ein unerwartetes Ergebnis.
Der zweite häufige Fall ist genau umgekehrt: Ein Ergebnis, das keine Union enthält, obwohl Distribution eigentlich erwartet wurde, oft weil der Typparameter versehentlich in ein Objekt, Array oder eine Funktion eingebettet wurde, wodurch die Naked-Type-Parameter-Regel nicht mehr erfüllt ist. Ein bewusster Blick auf die exakte syntaktische Position des Typparameters in der Bedingung löst die überwiegende Mehrheit dieser Debugging-Fälle zuverlässig.
9. Distributive vs. Non-Distributive im Vergleich
Die folgende Tabelle fasst zusammen, wann distributives und wann nicht-distributives Verhalten die richtige Wahl ist, mit den zugehörigen syntaktischen Mustern.
| Anwendungsfall | Verhalten | Muster | Beispiel |
|---|---|---|---|
| Union-Mitglieder herausfiltern | Distributive | T extends U ? never : T |
Exclude, Extract |
| Transformation pro Mitglied | Distributive | T extends unknown ? Wrap<T> : never |
ToArrayNaked aus Abschnitt 2 |
| Union als Ganzes prüfen | Non-Distributive | [T] extends [U] ? X : Y |
IsStringOrNumberWrapped |
| never sicher behandeln | Non-Distributive | [T] extends [string] ? "yes" : "no" |
CheckWrapped aus Abschnitt 5 |
Die Tabelle zeigt: Die Entscheidung zwischen distributive und non-distributive Verhalten ist keine Geschmacksfrage, sondern hängt direkt davon ab, ob die Aufgabe eine Union Mitglied für Mitglied oder als unteilbares Ganzes behandeln soll. Beide Verhaltensweisen sind mit derselben Conditional-Type-Syntax erreichbar, der einzige Unterschied liegt in der Tupel-Verpackung des geprüften Typparameters.
Mironsoft
TypeScript-Architektur, Typsystem-Beratung und Refactoring
Unerklärliche Union-Typen in euren Utility-Types?
Wir analysieren bestehende Conditional Types auf ungewollte Distribution, beheben never-Fallstricke und schreiben Utility-Typen mit bewusster, dokumentierter Kontrolle über Distributionsverhalten.
Typsystem-Debugging
Analyse unerwarteter Union-Ergebnisse und never-Fallstricke in bestehenden Typen
Utility-Type-Bibliothek
Maßgeschneiderte Exclude-, Extract- und IsUnion-Varianten für euren Codebestand
Code-Review-Standards
Richtlinien für Naked vs. Wrapped Type Parameter in eurem Team etablieren
10. Zusammenfassung
Distributive Conditional Types entstehen immer dann, wenn ein nackter, unverpackter Typparameter in der Bedingung eines Conditional Types steht und mit einem Union-Typ instanziiert wird. Der Compiler wendet die Bedingung dann auf jedes Mitglied einzeln an und fügt die Ergebnisse zu einer neuen Union zusammen, ein Verhalten, das die Grundlage von Exclude und Extract bildet.
Das Tupel-Wrapping-Muster [T] extends [U] ? X : Y unterdrückt Distribution gezielt, sobald eine Union als unteilbares Ganzes geprüft werden soll, etwa bei IsUnion-Typen oder bei der sicheren Behandlung von never, das als leere Union sonst zu überraschenden Ergebnissen führt. Wer bei jedem eigenen Conditional Type bewusst zwischen nacktem und verpacktem Typparameter entscheidet, vermeidet die häufigsten Debugging-Fallstricke rund um distributive Conditional Types.
Distributive Conditional Types — Das Wichtigste auf einen Blick
Grundregel
Ein nackter Typparameter in der Bedingung distribuiert automatisch über Union-Typen, Mitglied für Mitglied.
Exclude & Extract
Beide eingebauten Utility-Typen basieren vollständig auf Distribution, ohne sie würden sie nicht funktionieren.
Distribution verhindern
Tupel-Wrapping [T] extends [U] behandelt die Union als einzelnen, unteilbaren Typ.
never-Fallstrick
Distribution über die leere Union never ergibt immer never, unabhängig vom Inhalt der Zweige.