Ein Union-Typ listet Alternativen mit | auf: Ein Wert vom Typ string | number ist entweder ein String oder eine Zahl. Mit Unions beschreibt TypeScript Werte, die berechtigterweise mehr als eine Form annehmen können, und der Compiler zwingt dich zu prüfen, welche Form du hast, bevor du etwas Formspezifisches verwendest.
In jedem Zweig der Prüfung mit typeof hat id einen einzigen Typ. Dieser Schritt heißt Narrowing, und er macht Unions praktisch nutzbar.
Nur gemeinsame Member sind erlaubt
Vor dem Einengen kannst du nur verwenden, was jeder Member der Union unterstützt. toString() gibt es auf Strings und Zahlen, das ist also in Ordnung; toUpperCase() gibt es nur auf Strings:
index.ts(3,16): error TS2339: Property 'toUpperCase' does not exist on type 'string | number'.
Property 'toUpperCase' does not exist on type 'number'.
Die zweite Zeile nennt den Member, dem die Eigenschaft fehlt. Dieselbe Regel gilt in die andere Richtung: Ein Wert vom Typ string | number kann nicht an einen als string typisierten Parameter übergeben werden (TS2345), weil er eine Zahl sein könnte. Eine Union akzeptiert mehr Werte und lässt dich dafür weniger mit ihnen tun, bis du prüfst.
Eine Union einengen
Narrowing nutzt gewöhnliche JavaScript-Prüfungen. TypeScript folgt dem Kontrollfluss und entfernt Member, sobald sie ausgeschlossen sind, sodass nach der letzten Prüfung nur ein Member übrig bleibt:
| Prüfung | Engt ein | Geeignet für |
|---|---|---|
typeof x === "string" | auf den primitiven Typ | string, number, boolean, bigint, symbol, undefined, function |
x === null, x === "a" | auf den verglichenen Wert | null, undefined, Literal-Member |
Array.isArray(x) | auf den Array-Member | Arrays |
x instanceof Date | auf die Klasse | Klasseninstanzen |
"meow" in x | auf Member, die die Eigenschaft haben | Objekttypen |
x.kind === "circle" | auf den Member mit diesem Tag | Discriminated Unions |
isCat(x) (gibt x is Cat zurück) | auf das, was die Funktion sagt | alles, eigene Logik |
Die vollständige Liste der Formen des Narrowing steht auf der Seite zu Type Narrowing.
Unions aus Literaltypen
Eine Union aus Literalwerten ist eine geschlossene Menge erlaubter Werte. Sie ist die häufigste Union in echtem Code:
type Status = "idle" | "loading" | "success" | "error";
type Dice = 1 | 2 | 3 | 4 | 5 | 6;
type Toggle = "on" | "off" | boolean; // boolean is itself true | false
let current: Status = "idle";
current = "loading"; // fine
current = "finished"; // error TS2322: Type '"finished"' is not assignable to type 'Status'.
Der Vergleich mit einem Literal engt ein: Nach if (current === "error") weiß der else-Zweig, dass current einer der anderen drei ist. Literal-Unions ersetzen in vielen Codebasen Enums; unter Literaltypen stehen as const und wie man eine solche Union aus einem Array ableitet.
Unions aus Objekttypen
Sind die Member Objekttypen, sind Eigenschaften, die alle teilen, direkt verfügbar. Für den Rest prüfst du mit in, ob die Eigenschaft existiert:
Bei Unions aus mehreren Objektformen ist ein gemeinsames Literal-Feld wie kind: "cat" / kind: "fish" das sauberere Muster. Die Prüfung dieser einen Eigenschaft engt das ganze Objekt ein, und ein switch darüber lässt sich auf Vollständigkeit prüfen. Dieses Muster ist eine Discriminated Union.
Arrays und Unions
Wo die Klammern stehen, ändert die Bedeutung komplett:
| Typ | Bedeutet | Beispielwert |
|---|---|---|
(string | number)[] | ein Array, dessen Elemente jeweils ein String oder eine Zahl sind | [1, "two", 3] |
string[] | number[] | ein Array nur aus Strings oder ein Array nur aus Zahlen | ["a", "b"] |
string | number[] | ein String oder ein Array von Zahlen (| bindet schwächer als []) | "text" |
Beim Durchlaufen eines (string | number)[] ist jedes Element die Union und muss eingeengt werden, wie im Callback von reduce oben. Methoden wie map und filter funktionieren auch auf einem string[] | number[], wobei der Callback string | number bekommt.
Unions mit null und undefined
Die häufigste Union überhaupt ist „ein Wert oder nichts“: string | null, User | undefined. Das geben Array.prototype.find und Map.prototype.get zurück, und eine optionale Eigenschaft name?: string liest sich als string | undefined. Wie du sie mit ?., ?? und Null-Prüfungen behandelst, steht auf einer eigenen Seite: null und undefined.
Um Member auf Typebene aus einer vorhandenen Union zu entfernen, nimm die eingebauten Utilities: Exclude<"a" | "b" | "c", "a"> ist "b" | "c", und NonNullable<string | null> ist string.
Häufig gestellte Fragen
Was ist ein Union-Typ in TypeScript?
Ein Typ aus mehreren Alternativen, verbunden mit |. Ein Wert vom Typ string | number kann ein String oder eine Zahl sein. Der Compiler lässt dich nur verwenden, was alle Member gemeinsam haben, bis du den Wert mit einer Prüfung wie typeof value === "string" auf einen Member einengst.
Warum sagt TypeScript, eine Eigenschaft existiere auf einem Union-Typ nicht?
Weil mindestens einem Member der Union sie fehlt. Der Fehler TS2339, zum Beispiel Property 'toUpperCase' does not exist on type 'string | number', bedeutet, dass der Wert eine Zahl sein könnte, und die hat kein toUpperCase. Enge zuerst ein (typeof, in, Array.isArray, instanceof oder eine Prüfung auf die Diskriminante) und verwende dann die Eigenschaft des jeweiligen Members.
Wie deklariere ich ein Array, das mehr als einen Typ enthält?
Setze die Union in Klammern: (string | number)[] oder Array<string | number>, wobei jedes Element einer der beiden Typen sein kann. string[] | number[] ist etwas anderes: Das ganze Array besteht nur aus Strings oder nur aus Zahlen. Ohne Klammern bedeutet string | number[] einen String oder ein Array von Zahlen.
Was ist der Unterschied zwischen einem Union- und einem Intersection-Typ?
Eine Union A | B ist ein Wert, der einer der Typen ist, du kannst also nur verwenden, was sie gemeinsam haben. Eine Intersection A & B ist ein Wert, der beides zugleich ist, also hat sie alle Member beider. Bei Objekttypen akzeptiert A | B mehr Werte und A & B verlangt mehr Eigenschaften.
Wie prüfe ich, welcher Typ ein Union-Wert ist?
Mit einer Prüfung zur Laufzeit, die TypeScript versteht: typeof x === "string" für primitive Werte, Array.isArray(x) für Arrays, x instanceof Date für Klassen, "prop" in x für Objektformen oder x.kind === "circle", wenn die Member ein gemeinsames Literal-Tag haben. Für eigene Logik schreibst du eine Type-Guard-Funktion, die x is T zurückgibt.