navigation Navigation


Inhaltsverzeichnis

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.

    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.

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

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

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

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

    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.

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

    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.