Union & Intersection Types
Union- und Intersection-Types in TypeScript bieten leistungsstarke Möglichkeiten, verschiedene Typen flexibel zu kombinieren oder zu verschneiden. Sie ermöglichen die präzise Modellierung komplexer Strukturen und fördern die Typsicherheit in modernen Anwendungen. Durch den gezielten Einsatz dieser Typkonstrukte lassen sich vielseitige und robuste Typdefinitionen erstellen, die den Entwicklungsprozess effizienter und sicherer gestalten.
Inhaltsverzeichnis
Union Types (ODER)
Union Types stellen ein mächtiges Feature von TypeScript dar. Sie erlauben es, dass ein Wert einen von mehreren möglichen Typen haben kann. Dies spiegelt die Realität vieler JavaScript-Patterns wider und macht sie typischer.
Ein Union Type wird mit dem Pipe-Symbol (|
) erstellt und bedeutet “dieser Typ ODER jener Typ”.
Ein minimales Beispiel zu Beginn.
let id: string | number;
id = "ABC123"; // Ok
id = 123456; // Ok
// ❌ Das würde einen Fehler produzieren
id = true; // Error: Type 'boolean' is not assignable
Warum Union Types verwenden?
Union Types lösen reale Programmierprobleme elegant.
In JavaScript gibt es keine Typ-Garantien. Der Entwickler muss zur Laufzeit prüfen, welcher Typ id
hat (z.B. mit typeof
), sonst riskiert er Laufzeitfehler (z.B. undefined is not a function
). Ebenfalls kann die IDE hier nicht unterstützen.
function processId(id) {
// ...
}
In diesem Beispiel drüber könnte die id
ein string
, number
, boolean
oder sonst ein anderer Typ sein. Eine manuelle Prüfung wäre erforderlich.
TypeScript kann dieses Problem deutlich besser lösen.
function processId(id: string | number): string {
// TypeScript erzwingt die Behandlung beider Typen
if (typeof id === "string") {
// id ist garantiert ein string
return id.toUpperCase();
} else {
// id ist garantiert eine number
return id.toString();
}
}
Folgende Vorteile zur Verwendung von TypeScript mit Union Types sind zu erwähnen:
- Typen-Sicherheit: Die Funktion akzeptiert nur
string
odernumber
. - Compiler-Erzwingung: TypeScript verlangt, dass beide Fälle (
string
undnumber
) behandelt werden. - Keine unerwarteten Laufzeitfehler: Der Code ist robuster, da alle Typen explizit abgedeckt sind.
Type Narrowing mit Union Types
TypeScript versteht, wie sich Typen durch Bedingungen verengen.
Einfaches Type Narrowing mit typeof
.
TypeScript kann den Typ einer Variable automatisch einschränken (Type Narrowing), wenn eine Typ-Überprüfung durchgeführt wird. Dies ermöglicht eine typsichere Verarbeitung von Union Types, ohne dass explizite Type Assertions (as
) nötig sind.
function formatValue(value: string | number | boolean): string {
if (typeof value === "string") {
// Hier ist value garantiert ein string
return `String: "${value.trim()}"`;
} else if (typeof value === "number") {
// Hier ist value garantiert eine number
return `Number: "${value.toFixed(2)}" €`;
} else {
// Hier ist value garantiert ein boolean
return `Boolean: ${value ? "✅" : "❌"}`;
}
}
console.log(formatValue(" hello "));
console.log(formatValue(42.52423));
console.log(formatValue(false));
String: "hello"
Number: "42.52" €
Boolean: ❌
Discriminated Unions (Power-Pattern)
Discriminated Unions (auch Tagged Unions) nutzen ein gemeinsames Literal-Feld (z.B. status
, kind
oder type
), um zwischen verschiedenen Objekttypen zu unterscheiden. Gut nützlich für komplexe Zustände.
Hier ein Beispiel dazu. In diesem Beispiel werden verschiedene Interfaces definiert, die alle ein gemeinsames Feld (eine gemeinsame Eigenschaft) haben. Anhand diesem Feld wird der Typ unterschieden.
interface I_LoadingState {
status: "loading";
progress: number;
}
interface I_SuccessState<T> {
status: "success";
data: T;
}
interface I_ErrorState {
status: "error";
error: Error;
}
type T_AsyncState<T> = I_LoadingState | I_SuccessState<T> | I_ErrorState;
function renderState<T>(state: T_AsyncState<T>): string {
switch (state.status) {
case "loading":
// Hier ist 'state' = I_LoadingState
return `⏳ ${state.progress}%`;
case "success":
// Hier ist 'state' = I_SuccessState<T>
return `✅ ${JSON.stringify(state.data)}`;
case "error":
// Hier ist 'state' = I_ErrorState
return `❌ ${state.error.message}`;
default:
return;
}
}
const states: T_AsyncState<string>[] = [
{ status: "loading", progress: 75 },
{ status: "success", data: "API Response" },
{ status: "error", error: new Error("Network timeout") }
];
states.forEach(state => console.log(renderState(state)));
⏳ 75%
✅ "API Response"
❌ Network timeout
In diesem Beispiel das Feld status
der Diskriminator. Jeder Typ hat ein eindeutiges Literal: "loading"
, "success"
, "error"
. TypeScript nutzt dies zur exakten Typ-Verengung im switch
-Statement.
Operator in
- Typ-Verengung bei Objekten
Die Herangehensweise stellt eine Alternative zu Discriminated Unions dar, wenn Objekte kein gemeinsames Tag-Feld haben.
Wir schauen uns es in einem Beispiel an.
interface I_Cat {
meow(): void;
purrVolume: number;
}
interface I_Dog {
bark(): void;
breed: string;
}
function interact(pet: I_Cat | I_Dog) {
if ("meow" in pet) {
pet.meow();
console.log("Schnurr-Lautstärke:", pet.purrVolume);
} else {
pet.bark();
console.log("Rasse:", pet.breed);
}
}
const cat: I_Cat = {
meow: () => {
console.log("Run meow");
},
purrVolume: 3.4
};
const dog: I_Dog = {
bark: () => {
console.log("Run bark");
},
breed: "Default"
};
interact(cat);
interact(dog);
Run meow
Schnurr-Lautstärke: 3.4
Run bark
Rasse: Default
Union Types mit Literal Types
Literal Types sind konkrete Werte, die als Typen verwendet werden können. Kombiniert mit Union Types ermöglichen sie präzise Typdefinitionen, bei denen nur bestimmte Werte erlaubt sind.
Dies ist besonders nützlich für:
- Konfigurationsobjekte: z.B. Button-Styles, API-Methoden
- Zustandsmanagement: z.B. Redux-Actions, FSM-Zustände usw.
Hier ein einfaches Beispiel.
type T_Direction = "north" | "south" | "east" | "west";
const moves: T_Direction[] = [];
function move(direction: T_Direction): void {
console.log(`Moving ${direction}`);
switch (direction) {
case "north":
console.log("⬆️");
break;
case "south":
console.log("⬇️");
break;
case "east":
console.log("➡️");
break;
case "west":
console.log("⬅️");
break;
default:
break;
}
moves.push(direction);
}
move("north");
move("east");
move("east");
move("south");
console.log(moves);
Moving north
⬆️
Moving east
➡️
Moving east
➡️
Moving south
⬇️
[ 'north', 'east', 'east', 'south' ]
Und hier noch ein weiteres Beispiel wie man Literal Types in Kombination mit Union Types verwenden kann.
type I_ButtonSize = "small" | "medium" | "large";
type I_ButtonStyle = "primary" | "secondary" | "danger";
function getButtonStyles(size: I_ButtonSize, style: I_ButtonStyle): string {
const sizes = {
small: "8xp 12px",
medium: "12px 16px",
large: "16px 24px"
};
const styles = {
primary: "blue",
secondary: "gray",
danger: "red"
};
return `padding: ${sizes[size]}; background: ${styles[style]}`;
}
console.log(getButtonStyles("medium", "danger"));
console.log(getButtonStyles("large", "primary"));
padding: 12px 16px; background: red
padding: 16px 24px; background: blue
Intersection Types (UND)
Was sind Intersection Types?
Intersection Types kombinieren mehrere Typen zu einem. Sie werden mit dem Ampersand (&
) erstellt und bedeuten “dieser Typ UND jener Typ”.
Der neue, kombinierte Typ muss alle Eigenschaften der Einzeltypen erfüllen.
// Basis-Typen
type T_Named = { name: string };
type T_Aged = { age: number };
// Kombination aus beiden Typen
type T_Person = T_Named & T_Aged;
// ✅ Ok - alle Eigenschaften vorhanden
const personOne: T_Person = {
name: "John",
age: 30
};
// ❌ Fehler - Eigenschaft "age" fehlt.
const personTwo: T_Person = {
name: "Tom"
};
Person
erfordert sowohl name
(string
) als auch age
(number
). Fehlende Eigenschaften führen zum Kompilierungsfehler.
Warum Intersection Types verwenden?
Intersection Types ermöglichen eine modulare Typkomposition.
- Wiederverwendbarkeit: Kleine, spezialisierte Typen können kombiniert werden.
- Klare Kommunikation: Typen erzwingen explizit benötigte Eigenschaften.
- Erweiterbarkeit: Neue Funktionalität lässt sich durch zusätzliche Typen hinzufügen.