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.

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.

TypeScript Beispiel
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.

TypeScript JavaScript Version
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.

TypeScript TypeScript Version
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 oder number.
  • Compiler-Erzwingung: TypeScript verlangt, dass beide Fälle (string und number) 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.

TypeScript Beispiel - typeof
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));
Output
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.

TypeScript Beispiel
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)));
Output
⏳ 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.

TypeScript Beispiel - in
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);
Output
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.

TypeScript 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);
Output
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.

TypeScript Beispiel
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"));
Output
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.

TypeScript Beispiel
// 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.
/ Weiter

Zurück zu TypeScript

Zur Übersicht