navigation Navigation


Inhaltsverzeichnis

Object Utility Types


Object Utility Types in TypeScript wie Partial, Required, Record und Pick ermöglichen eine flexible und effiziente Manipulation von Objekttypen. Sie vereinfachen die Anpassung bestehender Typen, fördern die Wiederverwendbarkeit und erhöhen die Typsicherheit in komplexen Anwendungen. Durch den gezielten Einsatz dieser Utilities lassen sich Schnittstellen präzise modellieren und der Entwicklungsprozess deutlich optimieren.

Inhaltsverzeichnis

    Einführung

    TypeScript Object Utility Types sind vordefinierte, generische Typen, die es ermöglichen, neue Typen basierend auf bestehenden Objekttypen zu erstellen. Sie funktionieren wie “Werkzeuge” oder “Funktionen” auf Typebene, die bestehende Typen transformieren, modifizieren oder daraus neue Typen ableiten.

    Warum Object Utility Types?

    In der modernen JavaScript/TypeScript Entwicklung arbeitet man ständig mit komplexen Objekt-Strukturen. Öft benötigt man Variationen eines bestehenden Typs. Mal sollen alle Eigenschaften optional sein, mal nur bestimmte ausgewählt werden oder man möchte alle Eigenschaften als schreibgeschützt markieren. Ohne Object Utility Types müsste man diese Variationen manuell definieren.

    Das würde zu Folgendem führen:

    • Code-Duplikation
    • Wartungsprobleme bei Änderungen des ursprünglichen Typs
    • Inkonsistenzen zwischen verwandten Typen
    • Erhöhter Aufwand bei der Typ-Definition

    Mehrwert

    Object Utility Types liefern also folgende kräftige Vorteile:

    • Typ-Sicherheit: Sie stellen sicher, dass abgeleitete Typen automatisch konsistent bleiben, wenn sich der ursprüngliche Typ ändert.
    • DRY-Prinzip: Einmal definierte Typen können in verschiedenen Variationen wiederverwendet werden.
    • Wartbarkeit: Änderungen an Basis-Typen propagieren automatisch zu allen abgeleiteten Typen.
    • Lesbarkeit: Der Code wird selbstdokumentierend, da die Intention durch den Utility Typ klar wird.
    • Produktivität: Schnellere Entwicklung durch weniger manuelle Typ-Definition.

    Object Utility Types

    Partial<T> - Alle Eigenschaften optional machen

    Partial<T> macht alle Eigenschaften eines Typs <T> optional. Dies ist besonders nützlich, wenn man Updates oder partielle Daten verarbeiten muss.

    Syntax
    Partial<T>
    Implementierung
    type Partial<T> = {
        [P in keyof T]?: T[P]
    };

    Hier ein praktisches Beispiel.

    Beispiel
    interface User {
        id: number;
        name: string;
        email: string;
        age: number;
    }
    
    // Normalen Benutzer erstellen
    const user: User = {
        id: 1,
        name: "John",
        email: "john@mail.com",
        age: 30
    };
    
    // Neuen Benutzer auf Basis von 'User'
    // mit weniger Eigenschaften erzeugen
    const userPartial: Partial<User> = {
        email: "new@mail.com"
    };
    
    // Ausgabe von beiden Objekten
    console.log(user);
    console.log(userPartial);
    { id: 1, name: 'John', email: 'john@mail.com', age: 30 }
    { email: 'new@mail.com' }

    Hier ein weiteres Beispiel mit Verwendung in einer Funktion.

    Beispiel
    interface I_User {
        id: number;
        name: string;
        email: string;
        age: number;
        active: boolean;
    }
    
    // Mit Partial einen Typ für Updates erstellen
    type T_UserUpdate = Partial<I_User>;
    
    // Ein Äquivalent zu T_UserUpdate (nur zum Zeigen)
    type T_UserUpdateEquivalent = {
        id?: number;
        name?: string;
        email?: string;
        age?: string;
        active?: boolean;
    };
    
    // User-Liste
    const users: I_User[] = [
        {
            id: 1,
            name: "John",
            email: "john@mail.com",
            age: 30,
            active: true
        },
        {
            id: 2,
            name: "Tom",
            email: "tom@mail.com",
            age: 32,
            active: false
        },
        {
            id: 3,
            name: "Alice",
            email: "alice@mail.com",
            age: 40,
            active: true
        }
    ];
    
    // Hilfs-Funktion - Benutzer abrufen
    function getUserById(userId: number): I_User | undefined {
        return users.find(u => u.id === userId);
    }
    
    // Funktion zur Aktualisierung der Benutzer
    function updateUser(userId: number, updates: T_UserUpdate): I_User {
        // Hier kann man sicher sein, dass nur
        // gültige User-Eigenschaften übergeben werden, aber
        // sind alle optional
    
        const existingUser = getUserById(userId);
    
        return {
            ...existingUser,
            ...updates
        };
    }
    
    // Verwendung
    console.log(updateUser(1, { name: "Max" }));
    console.log(updateUser(1, { email: "max@mail.com", age: 31 }));
    console.log(updateUser(2, { active: true }));
    console.log(updateUser(3, {}));
    { id: 1, name: 'Max', email: 'john@mail.com', age: 30, active: true }
    { id: 1, name: 'John', email: 'max@mail.com', age: 31, active: true }
    { id: 2, name: 'Tom', email: 'tom@mail.com', age: 34, active: true }
    {
    id: 3,
    name: 'Alice',
    email: 'alice@mail.com',
    age: 40,
    active: true
    }

    Required<T> - Alle Eigenschaften verpflichtend machen

    Required<T> ist das Gegenteil von Partial<T> und macht alle Eigenschaften eines Typs verpflichtend, auch wenn sie ursprünglich optional waren.

    Syntax
    Required<T>
    Implementierung
    type Required<T> = {
        [P in keyof T]-?: T[P]
    };

    Nun schauen wir uns die Funktionsweise an einem praktischen Beispiel an.

    Beispiel
    interface I_ApiConfig {
        baseUrl?: string;
        timeout?: number;
        retries?: number;
        headers?: Record<string, string>;
    }
    
    // Finale Konfiguration soll alle Werte haben
    type T_ApiConfigFull = Required<I_ApiConfig>;
    
    // Äquivalent zu T_ApiConfigFull (nur zum Zeigen)
    type T_ApiConfigFullEquivalent = {
        baseUrl: string;
        timeout: number;
        retries: number;
        headers: Record<string, string>;
    };
    
    class ApiConfigBuilder {
    
        private config: Partial<I_ApiConfig> = {};
    
        setBaseUrl(url: string): this {
            this.config.baseUrl = url;
            return this;
        }
    
        setTimeout(ms: number): this {
            this.config.timeout = ms;
            return this;
        }
    
        setRetries(count: number): this {
            this.config.retries = count;
            return this;
        }
    
        setHeaders(headers: Record<string, string>): this {
            this.config.headers = headers;
            return this;
        }
    
        build(): Required<I_ApiConfig> {
            if (!this.config.baseUrl) throw new Error("baseUrl is required");
            if (this.config.timeout === undefined) throw new Error("timeout is required");
            if (this.config.retries === undefined) throw new Error("retries is required");
            if (!this.config.headers) throw new Error("headers is required");
    
            return this.config as T_ApiConfigFull;
        }
    
    }
    
    // Verwendung
    const configOne = new ApiConfigBuilder()
        .setBaseUrl("http://localhost:9000")
        .setTimeout(5000)
        .setRetries(3)
        .setHeaders({ "Content-Type": "application/json" })
        .build();
    
    console.log(configOne);
    
    try {
        const configTwo = new ApiConfigBuilder()
            .setBaseUrl("http://localhost:8000")
            .setRetries(2)
            .setHeaders({ "Content-Type": "application/json" })
            .build();
    } catch (error) {
        console.log("Fehler:", error.message);
    }
    {
        baseUrl: 'http://localhost:9000',
        timeout: 5000,
        retries: 3,
        headers: { 'Content-Type': 'application/json' }
    }
    Fehler: timeout is required

    In diesem Beispiel haben wir zwei Objekt vom Typ T_ApiConfigFull definiert. Im ersten Objekt sind alle Felder vorhanden. Im zweiten Objekt wurde ein Feld nicht übergeben/gesetzt. Dies führt zu einem Fehler, das die Methode build() vom Typ T_ApiConfigFull (was gleich Required<I_ApiConfig> ist) zurückgibt und vorher das Vorhandensein aller Felder prüft.

    Schauen wir uns ein weiteres Beispiel an.

    Beispiel
    interface I_CreateUserInput {
        name?: string;
        email?: string;
        password?: string;
    }
    
    /**
    * Create user
    * ---
    * Nach Validierung und Verarbeitung
    * sind alle Felder vorhanden.
    * ---
    * @returns {Required<I_CreateUserInput>}
    */
    function createUser(input: I_CreateUserInput): Required<I_CreateUserInput> {
        if (!input.name) throw new Error("Name is required");
        if (!input.email) throw new Error("Email is required");
        if (!input.password) throw new Error("Password is required");
        
        return input as Required<I_CreateUserInput>;
    }
    
    const userOne = createUser({
        name: "John",
        email: "john@mail.com",
        password: "12345"
    });
    
    console.log(userOne);
    
    try {
        const userTwo = createUser({
            name: "Tom",
            email: "tom@mail.com"
        });
    } catch (error) {
        console.log("Fehler:", error.message);
    }
    { name: 'John', email: 'john@mail.com', password: '12345' }
    Fehler: Password is required

    In diesem Beispiel erwartet die Funktion createUser() ein Objekt mit allen Feldern, welche im Interface I_CreateUserInput definiert sind. Andernfalls, wie im Fall von userTwo, wird ein Fehler geworfen.


    Readonly<T> - Alle Eigenschaften schreibgeschützt machen

    Readonly<T> macht alle Eigenschaften eines Typs schreibgeschützt. Dies ist nützlich für unveränderliche Daten-Strukturen oder um versehentliche Modifikationen zu verhindern.

    Syntax
    Readonly<T>
    Implementierung
    type Readonly<T> = {
        readonly [P in keyof T]: T[P]
    };

    Beispiel 1

    Schauen wir uns ein praktisches Beispiel an, das die Verwendung von Readonly<T> verdeutlicht.

    Beispiel Readonly (1)
    interface I_Product {
        id: number;
        name: string;
        price: number;
        category: string;
    }
    
    // Schreibgeschützte Version
    type T_ProductReadonly = Readonly<I_Product>;
    
    // Äquivalent zu T_ProductReadonly (als Beispiel)
    type T_ProductReadonlyEquivalent = {
        readonly id: number;
        readonly name: string;
        readonly price: number;
        readonly category: string;
    };
    
    /**
    * Fetch product
    * ---
    * @param {number} id - ID of the product
    * ---
    * @returns {T_ProductReadonly}
    */
    function fetchProduct(id: number): T_ProductReadonly {
        const product = {
            id,
            name: "Laptop",
            price: 1099,
            category: "Electronis"
        };
        
        return product;
    }
    
    const product = fetchProduct(1);
    console.log(product);
    
    // Versuch etwas zu ändern
    // product.price = 999;
    // Cannot assign to 'price' because it is a read-only property.
    { id: 1, name: 'Laptop', price: 1099, category: 'Electronis' }

    Beispiel 2

    Lasst uns ein weiteres, umfangreicheres Beispiel aufbauen und Readonly<T> verwenden.

    Beispiel Readonly (2)
    interface I_Product {
        id: string;
        name: string;
        price: number;
        inStock: boolean;
    }
    
    interface I_User {
        id: string;
        name: string;
        email: string;
    }
    
    interface I_CartItem {
        productId: string;
        quantity: number;
    }
    
    interface I_AppState {
        user: I_User | null;
        products: I_Product[];
        cart: I_CartItem[];
        isLoading: boolean;
    }
    
    // Schreibgeschützte Version
    type T_AppStateReadonly = Readonly<I_AppState>;
    
    class Store {
        
        private state: I_AppState = {
            user: null,
            products: [],
            cart: [],
            isLoading: false
        };
        
        /**
         * Get current state
        * ---
        * @returns {T_AppStateReadonly}
        */
        getState(): T_AppStateReadonly {
            return this.state;
        }
        
        /**
         * Set state
        * ---
        * @param newState - New state
        */
        private setState(newState: Partial<I_AppState>): void {
            this.state = { ...this.state, ...newState };
        }
        
        /**
         * Set loading status
        * ---
        * @param {boolean} loading - New loading status
        */
        setLoading(loading: boolean): void {
            this.setState({ isLoading: loading });
        }
        
        /**
         * Login user
        * ---
        * @param {I_User} user - User data
        */
        loginUser(user: I_User): void {
            this.setState({ user, isLoading: false });
        }
        
        /**
         * Logout user
        */
        logoutUser(): void {
            this.setState({ user: null, cart: [] });
        }
    
        /**
        * Load products
        * ---
        * @param {I_Product[]} products - Products to load
        */
        loadProducts(products: I_Product[]): void {
            this.setState({ products, isLoading: false });
        }
        
        /**
        * Add to cart
        * ---
        * @param {string} productId - ID of the product
        * @param {number} quantity - Quantity to add
        */
        addToCart(productId: string, quantity: number = 1): void {
            const existingItem = this.state.cart.find(i => i.productId === productId);
            const newCart = existingItem
                ? this.state.cart.map(item =>
                    item.productId === productId
                        ? { ...item, quantity: item.quantity + quantity }
                        : item
                )
                : [...this.state.cart, { productId, quantity }];
                
            this.setState({ cart: newCart });
        }
        
        /**
        * Remove from cart
        * ---
        * @param {string} productId - ID of the product to remove
        */
        removeFromCart(productId: string): void {
            const newCart = this.state.cart.filter(i => i.productId !== productId);
            this.setState({ cart: newCart });
        }
        
    }
    
    const store = new Store();
    
    // Initialer Zustand
    console.log("Initialer Zustand:", store.getState());
    console.log("\n--- --- ---\n");
    
    // Benutzer anmelden
    store.setLoading(true);
    store.loginUser({
        id: "u_001",
        name: "John",
        email: "john@mail.com"
    });
    console.log("Nach Login:", store.getState());
    console.log("\n--- --- ---\n");
    
    // Produkte laden
    store.setLoading(true);
    store.loadProducts([
        { id: "p_001", name: "Laptop", price: 999, inStock: true },
        { id: "p_002", name: "Smartphone", price: 699, inStock: true },
        { id: "p_003", name: "Headphones", price: 149, inStock: false }
    ]);
    console.log("Produkte geladen:", store.getState());
    console.log("\n--- --- ---\n");
    
    // Warenkorb Aktionen
    store.addToCart("p_001");
    store.addToCart("p_002", 2);
    console.log("Im Warenkorb:", store.getState());
    console.log("\n--- --- ---\n");
    
    store.removeFromCart("p_002");
    console.log("Neuer Warenkorb:", store.getState());
    console.log("\n--- --- ---\n");
    
    // Benutzer abmelden
    store.logoutUser();
    console.log("Nach Logout:", store.getState());
    Initialer Zustand: { user: null, products: [], cart: [], isLoading: false }
    
    --- --- ---
    
    Nach Login: {
    user: { id: 'u_001', name: 'John', email: 'john@mail.com' },
    products: [],
    cart: [],
    isLoading: false
    }
    
    --- --- ---
    
    Produkte geladen: {
    user: { id: 'u_001', name: 'John', email: 'john@mail.com' },
    products: [
        { id: 'p_001', name: 'Laptop', price: 999, inStock: true },
        { id: 'p_002', name: 'Smartphone', price: 699, inStock: true },
        { id: 'p_003', name: 'Headphones', price: 149, inStock: false }
    ],
    cart: [],
    isLoading: false
    }
    
    --- --- ---
    
    Im Warenkorb: {
    user: { id: 'u_001', name: 'John', email: 'john@mail.com' },
    products: [
        { id: 'p_001', name: 'Laptop', price: 999, inStock: true },
        { id: 'p_002', name: 'Smartphone', price: 699, inStock: true },
        { id: 'p_003', name: 'Headphones', price: 149, inStock: false }
    ],
    cart: [
        { productId: 'p_001', quantity: 1 },
        { productId: 'p_002', quantity: 2 }
    ],
    isLoading: false
    }
    
    --- --- ---
    
    Neuer Warenkorb: {
    user: { id: 'u_001', name: 'John', email: 'john@mail.com' },
    products: [
        { id: 'p_001', name: 'Laptop', price: 999, inStock: true },
        { id: 'p_002', name: 'Smartphone', price: 699, inStock: true },
        { id: 'p_003', name: 'Headphones', price: 149, inStock: false }
    ],
    cart: [ { productId: 'p_001', quantity: 1 } ],
    isLoading: false
    }
    
    --- --- ---
    
    Nach Logout: {
    user: null,
    products: [
        { id: 'p_001', name: 'Laptop', price: 999, inStock: true },
        { id: 'p_002', name: 'Smartphone', price: 699, inStock: true },
        { id: 'p_003', name: 'Headphones', price: 149, inStock: false }
    ],
    cart: [],
    isLoading: false
    }

    Beispiel 3

    Im nächten Beispiel bauen wir eine kleine Funktion zum Herstellen der Datenbank-Verbindung auf. Dies ist lediglich eine vereinfachte Version, die ebenfalls die Verwendung von Readonly<T> demonstriert.

    Beispiel Readonly (3)
    interface I_DatabaseConfig {
        host: string;
        port: number;
        database: string;
        ssl: boolean;
    }
    
    /**
    * Create database connection
    * ---
    * @param {Readonly<I_DatabaseConfig>} config - Database configuration
    */
    function createDatabaseConnection(config: Readonly<I_DatabaseConfig>) {
        console.log(`Connecting to: ${config.host}:${config.port}`);
        
        // Für Modifikation expliti ein neues Objekt erstellen
        const newConfig = { ...config, port: 3007 };
        return newConfig;
    }
    
    const conn = createDatabaseConnection({ host: "localhost", port: 3306, database: "my_db", ssl: true });
    
    console.log(conn);
    Connecting to: localhost:3306
    { host: 'localhost', port: 3007, database: 'my_db', ssl: true }

    Pick<T, K> - Bestimmte Eigenschaften auswählen

    Pick<T, K> erstellt einen neuen Typ, indem es nur die angegebenen Eigenschaften K aus dem Typ T auswählt.

    Syntax
    Pick<T, K extends keyof T>
    Implementierung
    type Pick<T, K extends keyof T> = {
        [P in K]: T[P]
    };

    Beispiel 1

    Zuerst werfen wir einen Blick auf ein einfaches Beispiel.

    Beispiel Pick (1)
    interface I_User {
        id: number;
        username: string;
        email: string;
        salary: number;
    }
    
    type T_UserLight = Pick<I_User, "id" | "username">;
    
    const userFull: I_User = {
        id: 1,
        username: "john",
        email: "john@mail.com",
        salary: 50000
    };
    
    const userLight: T_UserLight = {
        id: 2,
        username: "tom"
    };
    
    console.log(userFull);
    console.log("--- --- ---")
    console.log(userLight);
    { id: 1, username: 'john', email: 'john@mail.com', salary: 50000 }
    --- --- ---
    { id: 2, username: 'tom' }

    In diesem Beispiel haben wir zwei Objekte (Benutzer). In einem Fall haben wir einen Benutzer vom Typ I_User definiert. Dieses Objekt muss das gesamte Interface und entsprechend auch alle Felder implementieren. Im zweiten Fall definieren wir einen Benutzer vom Typ T_UserLight. Hier müssen lediglich die beiden definierten Felder implementiert (vorhanden) sein.


    Beispiel 2

    Schauen wir uns die Funktionsweise anhand eines komplexeren Beispiels an. Wie bereits erwähnt, ermöglicht Pick<T, K> nur bestimmte Felder eines bereits vorhandenen Typs auszuwählen.

    Beispiel Pick (2)
    interface I_User {
        id: number;
        username: string;
        email: string;
        password: string;
        firstName: string;
        lastName: string;
        dateOfBirth: Date;
        address: string;
        phoneNumber: string;
        createdAt: Date;
        updatedAt: Date;
        isActive: boolean;
        role: "admin" | "moderator" | "user";
    }
    
    // Nur für Login benötigte Felder
    type T_LoginCredentials = Pick<I_User, "username" | "password">;
    
    // Äquivalent zu T_LoginCredentials (nur als Beispiel)
    type T_LoginCredentialsEquivalent = {
        username: string;
        password: string;
    };
    
    // Für öffentliche Profile (ohne sensitive Daten)
    type T_PublicProfile = Pick<I_User, "id" | "username" | "firstName" | "lastName">;
    
    // Für Benutzer-Liste im Admin-Panel
    type T_UserListItem = Pick<I_User, "id" | "username" | "email" | "isActive" | "role">;
    
    // Mock-Datenbank
    const usersDatabase: I_User[] = [
        {
            id: 1,
            username: "john",
            email: "john@example.com",
            password: "secure123",
            firstName: "John",
            lastName: "Doe",
            dateOfBirth: new Date("1990-01-01"),
            address: "123 Main St",
            phoneNumber: "555-1234",
            createdAt: new Date("2020-01-01"),
            updatedAt: new Date("2023-01-01"),
            isActive: true,
            role: "user"
        },
        {
            id: 2,
            username: "admin",
            email: "admin@example.com",
            password: "admin123",
            firstName: "Admin",
            lastName: "User",
            dateOfBirth: new Date("1985-05-15"),
            address: "456 Admin Ave",
            phoneNumber: "555-5678",
            createdAt: new Date("2019-01-01"),
            updatedAt: new Date("2023-06-01"),
            isActive: true,
            role: "admin"
        }
    ];
    
    /**
    * Authenticate user
    * ---
    * @param {string} username - Username
    * @param {string} password - Password
    * ---
    * @returns {Promise<string>}
    */
    async function authenticateUser(username: string, password: string): Promise<string> {
        const user = usersDatabase.find(u => u.username === username && u.password === password);
        
        if (!user) throw new Error("Invalid credentials");
        
        return `token-for-${user.id}`;
    }
    
    /**
    * Get user by ID
    * ---
    * @param {number} userId - ID of the user
    * ---
    * @returns {I_User}
    */
    function getUserById(userId: number): I_User {
        const user = usersDatabase.find(u => u.id === userId);
        
        if (!user) throw new Error("User not found");
        
        return user;
    }
    
    /**
    * Login
    * ---
    * @param {T_LoginCredentials} credentials - Login credentials
    * ---
    * @returns {Promise<string>}
    */
    async function login(credentials: T_LoginCredentials): Promise<string> {
        // Hier sind nur username und password verfügbar
        return authenticateUser(credentials.username, credentials.password);
    }
    
    /**
    * Get user profile
    * ---
    * @param {number} userId - ID of the user
    * ---
    * @returns {T_PublicProfile}
    */
    function getUserProfile(userId: number): T_PublicProfile {
        const user = getUserById(userId);
        
        // Explizite Auswahl verhindert versehentliche Preisgabe von Daten
        return {
            id: user.id,
            username: user.username,
            firstName: user.firstName,
            lastName: user.lastName
        };
    }
    
    /**
    * Get active users
    * ---
    * @returns {T_UserListItem[]}
    */
    function getActiveUsers(): T_UserListItem[] {
        return usersDatabase
            .filter(u => u.isActive)
            .map(u => ({
                id: u.id,
                username: u.username,
                email: u.email,
                isActive: u.isActive,
                role: u.role
            }));
    }
    
    // Beispiel - Ausführung
    async function runExample() {
        try {
            // Login (Beispiel)
            const token = await login({
                username: "john",
                password: "secure123"
            });
            console.log("Token:", token);
            console.log("\n--- --- ---\n");
            
            // Profil abrufen (Beispiel)
            const profile = getUserProfile(1);
            console.log("User profile:", profile);
            console.log("\n--- --- ---\n");
            
            // Admin-Liste (Beispiel)
            const activeUsers = getActiveUsers();
            console.log("Active users:", activeUsers);
        } catch (error) {
            if (error instanceof Error) {
                console.log("Fehler:", error.message);
            } else {
                console.log("Fehler:", error);
            }
        }
    }
    
    runExample();
    --- --- ---
    
    User profile: { id: 1, username: 'john', firstName: 'John', lastName: 'Doe' }
    
    --- --- ---
    
    Active users: [
    {
        id: 1,
        username: 'john',
        email: 'john@example.com',
        isActive: true,
        role: 'user'
    },
    {
        id: 2,
        username: 'admin',
        email: 'admin@example.com',
        isActive: true,
        role: 'admin'
    }
    ]

    Beispiel 3

    In diesem Beispiel wird wieder ein Basis-Interfaces definiert. Daraus werden unterschiedliche Typen gebildet, welche nur einen Ausschnitt an Eigenschaften des Basis-Interfaces führen und für unterschiedliche Zwecke verwendet werden.

    Beispiel Pick (3)
    interface I_ApiProduct {
        id: number;
        sku: string;
        name: string;
        description: string;
        price: number;
        cost: number;
        inventory: number;
        supplierId: number;
        isActive: boolean;
        createdAt: Date;
        updatedAt: Date;
    }
    
    // Für öffentliche API - ohne interne Informationen
    type T_PublicProduct = Pick<I_ApiProduct, "id" | "name" | "description" | "price" | "isActive">;
    
    // Für Inventar-Management
    type T_InventoryItem = Pick<I_ApiProduct, "id" | "sku" | "name" | "inventory" | "isActive">;
    
    // Für Einkauf
    type T_PurchaseItem = Pick<I_ApiProduct, "id" | "name" | "cost" | "supplierId" | "inventory">;
    
    // Mock-Datenbank
    const productsDatabase: I_ApiProduct[] = [
        {
            id: 1,
            sku: "prod_001",
            name: "Premium Kopfhörer",
            description: "Noise-Cancelling Köpfhörer mit exzellentem Klang",
            price: 299.99,
            cost: 120.50,
            inventory: 150,
            supplierId: 101,
            isActive: true,
            createdAt: new Date("2025-06-21"),
            updatedAt: new Date("2025-06-22")
        },
        {
            id: 2,
            sku: "prod_002",
            name: "Mechanische Tastatur",
            description: "RGB-beleuchtete mechanische Tastatur",
            price: 129.99,
            cost: 45.80,
            inventory: 75,
            supplierId: 102,
            isActive: true,
            createdAt: new Date("2025-06-21"),
            updatedAt: new Date("2025-06-22")
        },
        {
            id: 3,
            sku: "prod_003",
            name: "Veraltetes Produkt",
            description: "Dieses Produkt wird nicht mehr verkauft",
            price: 49.99,
            cost: 20.00,
            inventory: 10,
            supplierId: 103,
            isActive: false,
            createdAt: new Date("2022-11-05"),
            updatedAt: new Date("2025-06-20")
        }
    ];
    
    /**
    * Get all products
    * ---
    * @returns {I_ApiProduct[]}
    */
    function getAllProducts(): I_ApiProduct[] {
        return productsDatabase;
    }
    
    /**
    * Get public products
    * ---
    * @returns {T_PublicProduct[]}
    */
    function getPublicProducts(): T_PublicProduct[] {
        const allProducts = getAllProducts();
        
        return allProducts.map(product => ({
            id: product.id,
            name: product.name,
            description: product.description,
            price: product.price,
            isActive: product.isActive
        }));
    }
    
    /**
    * Get inventory items
    * ---
    * @returns {T_InventoryItem[]}
    */
    function getInventoryItems(): T_InventoryItem[] {
        const allProducts = getAllProducts();
        
        return allProducts.map(product => ({
            id: product.id,
            sku: product.sku,
            name: product.name,
            inventory: product.inventory,
            isActive: product.isActive
        }));
    }
    
    /**
    * Get purchase items
    * ---
    * @returns {T_PurchaseItem[]}
    */
    function getPurchaseItems(): T_PurchaseItem[] {
        const allProducts = getAllProducts();
        
        return allProducts
            .filter(product => product.inventory < 100)
            .map(product => ({
                id: product.id,
                name: product.name,
                cost: product.cost,
                supplierId: product.supplierId,
                inventory: product.inventory
            }));
    }
    
    // Ausführung
    function runExample() {
        try {
            // Öffentliche Produkte abrufen
            const publicProducts = getPublicProducts();
            console.log("Öffentliche Produkte");
            console.log(publicProducts);
            console.log("--- --- ---");
            
            // Inventar-Liste abrufen
            const inventoryItems = getInventoryItems();
            console.log("Inventar-Liste");
            console.log(inventoryItems);
            console.log("--- --- ---");
            
            // Einkaufsliste abrufen
            const purchaseItems = getPurchaseItems();
            console.log("Einkaufsliste");
            console.log(purchaseItems);
        } catch (error) {
            if (error instanceof Error) {
                console.log("Fehler:", error.message);
            } else {
                console.log("Fehler:", error);
            }
        }
    }
    
    runExample();
    Öffentliche Produkte
    [
        {
            id: 1,
            name: 'Premium Kopfhörer',
            description: 'Noise-Cancelling Köpfhörer mit exzellentem Klang',
            price: 299.99,
            isActive: true
        },
        {
            id: 2,
            name: 'Mechanische Tastatur',
            description: 'RGB-beleuchtete mechanische Tastatur',
            price: 129.99,
            isActive: true
        },
        {
            id: 3,
            name: 'Veraltetes Produkt',
            description: 'Dieses Produkt wird nicht mehr verkauft',
            price: 49.99,
            isActive: false
        }
    ]
    --- --- ---
    Inventar-Liste
    [
        {
            id: 1,
            sku: 'prod_001',
            name: 'Premium Kopfhörer',
            inventory: 150,
            isActive: true
        },
        {
            id: 2,
            sku: 'prod_002',
            name: 'Mechanische Tastatur',
            inventory: 75,
            isActive: true
        },
        {
            id: 3,
            sku: 'prod_003',
            name: 'Veraltetes Produkt',
            inventory: 10,
            isActive: false
        }
    ]
    --- --- ---
    Einkaufsliste
    [
        {
            id: 2,
            name: 'Mechanische Tastatur',
            cost: 45.8,
            supplierId: 102,
            inventory: 75
        },
        {
            id: 3,
            name: 'Veraltetes Produkt',
            cost: 20,
            supplierId: 103,
            inventory: 10
        }
    ]

    Beispiel 4

    Im letzten Beispiel zu Pick bauen wir eine Mini-Anwendung zur Erstellung neuer Benutzer. Auch in diesem Beispiel gibt es ein Basis-Interface (I_User). Davon wird mithilfe von Pick ein Ausschnitt an Eigenschaften verwendet, um einen Ableger-Typ zu definieren.

    Es wird ein schlankeres Interface I_UserCreate erstellt.

    Beispiel 4
    interface I_User {
        id: number;
        username: string;
        email: string;
        password: string;
        firstName: string;
        lastName: string;
        dateOfBirth: Date;
        address: string;
        phoneNumber: string;
        createdAt: Date;
        updatedAt: Date;
        isActive: boolean;
        role: "admin" | "moderator" | "user";
    }
    
    interface I_UserCreate {
        username: string;
        email: string;
        password: string;
        passwordConfirm: string;
        firstName: string;
        lastName: string;
        agreedToTerms: boolean;
    }
    
    // Für Backend ohne UI-spezifische Felder
    type T_UserCreateDto = Pick<I_UserCreate, "username" | "email" | "password" | "firstName" | "lastName">;
    
    // Mock-Datenbank
    let usersDatabase: I_User[] = [
        {
            id: 1,
            username: "existing_user",
            email: "existing@mail.com",
            password: "hashedPassword1",
            firstName: "Existing",
            lastName: "User",
            dateOfBirth: new Date("1990-01-01"),
            address: "123 Main St",
            phoneNumber: "555-1234",
            createdAt: new Date("2025-06-25"),
            updatedAt: new Date("2025-06-25"),
            isActive: true,
            role: "user"
        }
    ];
    
    /**
    * Save user to database
    * ---
    * @param {T_UserCreateDto} userData - New user data
    * ---
    * @returns {Promise<I_User>}
    */
    async function saveUserToDatabase(userData: T_UserCreateDto): Promise<I_User> {
        
        // Prüfe username
        if (usersDatabase.some(u => u.username === userData.username)) {
            throw new Error("Username already exists");
        }
    
        // Prüfe email
        if (usersDatabase.some(u => u.email === userData.email)) {
            throw new Error("Email already exists");
        }
        
        // Neuen Benutzer erstellen (mit Standardwerten)
        const newUser: I_User = {
            id: usersDatabase.length + 1,
            ...userData,
            dateOfBirth: new Date(),
            address: "",
            phoneNumber: "",
            createdAt: new Date(),
            updatedAt: new Date(),
            isActive: true,
            role: "user"
        };
        
        usersDatabase.push(newUser);
        return newUser;
    
    }
    
    /**
    * Validate user form
    * ---
    * @param {I_UserCreate} formData - Form data
    * ---
    * @returns {string[]}
    */
    function validateUserForm(formData: I_UserCreate): string[] {
        const errors: string[] = [];
        
        if (!formData.username) errors.push("Username is required");
        if (formData.username.length < 3) errors.push("Username must be at least 3 characters");
        if (!formData.email.includes("@")) errors.push("Invalid email format");
        if (formData.password.length < 6) errors.push("Password must be at least 6 characters");
        if (formData.password !== formData.passwordConfirm) errors.push("Passwords do not match");
        if (!formData.firstName) errors.push("Firstname is required");
        if (!formData.agreedToTerms) errors.push("You must agree to the terms");
        
        return errors;
    }
    
    /**
    * Create user
    * ---
    * @param {T_UserCreateDto} userData - User data
    * ---
    * @returns {Promise<I_User>}
    */
    async function createUser(userData: T_UserCreateDto): Promise<I_User> {
        return saveUserToDatabase(userData);
    }
    
    async function runExample() {
        try {
            // Simuliere Formular-Eingabe
            const formData: I_UserCreate = {
                username: "new_user",
                email: "new@mail.com",
                password: "hashedPassword2",
                passwordConfirm: "hashedPassword2",
                firstName: "New",
                lastName: "User",
                agreedToTerms: true
            };
            
            // Validiere Formular
            const errors = validateUserForm(formData);
            if (errors.length > 0) {
                console.log("Formular-Fehler:", errors);
                return;
            }
            
            // Für Backend-Request
            const userDto: T_UserCreateDto = {
                username: formData.username,
                email: formData.email,
                password: formData.password,
                firstName: formData.firstName,
                lastName: formData.lastName
            };
            
            console.log("Creating user:", userDto);
            
            // Benutzer erstellen
            const createdUser = await createUser(userDto);
            console.log("Benutzer erfolgreich erstellt:", {
                id: createdUser.id,
                username: createdUser.username,
                email: createdUser.email
            });
            
            // Alle Benutzer auslesen
            console.log("Alle Benutzer:", usersDatabase.map(u => ({
                id: u.id,
                username: u.username,
                email: u.email
            })));
        } catch (error) {
            console.log("Fehler:", error instanceof Error ? error.message : error);
        }
    }
    
    runExample();
    Erstelle Benutzer: {
        username: 'new_user',
        email: 'new@mail.com',
        password: 'hashedPassword2',
        firstName: 'New',
        lastName: 'User'
    }
    Benutzer erfolgreich erstellt: { id: 2, username: 'new_user', email: 'new@mail.com' }
    Alle Benutzer: [
        { id: 1, username: 'existing_user', email: 'existing@mail.com' },
        { id: 2, username: 'new_user', email: 'new@mail.com' }
    ]

    Omit<T, K> - Bestimmte Eigenschaften ausschließen

    Omit<T, K> ist das Gegenteil von Pick<T, K> und erstellt einen neuen Typ, indem es die angegebenen Eigenschaften K aus dem Typ T entfernt.

    Syntax
    Omit<T, K extends keyof T>
    Implementierung
    type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

    Beispiel 1

    Zuerst schauen wir uns ein minimales Beispiel an, um zu verstehen, wie Omit funktioniert. Im ersten Beispiel definieren wir zwei Benutzer-Typen. Einen Basis-Typ und einen angeleiteten Typen auf Basis vom Basis-Typ mit ein paar entfernten Eigenschaften.

    Beispiel 1
    interface I_User {
        id: number;
        username: string;
        email: string;
        password: string;
        createdAt: Date;
        updatedAt: Date;
        role: "admin" | "user";
    }
    
    type T_UserReduced = Omit<I_User, "id" | "password" | "updatedAt">;
    
    const userFull: I_User = {
        id: 1,
        username: "John",
        email: "john@mail.com",
        password: "john123",
        createdAt: new Date(),
        updatedAt: new Date(),
        role: "admin"
    };
    
    const userLight: T_UserReduced = {
        username: "tom",
        email: "tom@mail.com",
        createdAt: new Date(),
        role: "user"
    };
    
    console.log(userFull);
    console.log(userLight);
    {
        id: 1,
        username: 'John',
        email: 'john@mail.com',
        password: 'john123',
        createdAt: 2025-06-25T09:55:31.660Z,
        updatedAt: 2025-06-25T09:55:31.660Z,
        role: 'admin'
    }
    {
        username: 'tom',
        email: 'tom@mail.com',
        createdAt: 2025-06-25T09:55:31.660Z,
        role: 'user'
    }

    Wenn wir versuchen würden, beim reduzierten Typ des Benutzers eine Eigenschaft hinzuzufügen, die vorher mit Omit ausgeschlossen wurde, erhalten wir einen Fehler.

    Beispiel 1 (Mit Fehler)
    interface I_User {
        id: number;
        username: string;
        email: string;
        password: string;
        createdAt: Date;
        updatedAt: Date;
        role: "admin" | "user";
    }
    
    type T_UserReduced = Omit<I_User, "id" | "password" | "updatedAt">;
    
    // ❌ error TS2353: Object literal may only specify known properties, and 'id' does not exist in type 'T_UserReduced'
    const user: T_UserReduced = {
        id: 3,
        username: "alice",
        email: "alice@mail.com",
        createdAt: new Date(),
        role: "user"
    };

    Beispiel 2

    Im zweiten Beispiel definieren wir ein Basis-Interface für Benutzer (U_User). Dieses Interface beinhaltet alle Eigenschaften, die je für unterschiedliche Kontexte benötigt werden.

    Von diesem Basis-Typ werden weitere Typen definiert, die für die jeweiligen Fälle verwendet werden. Damit werden einige Eigenschaften entfernt.

    Beispiel 2
    interface I_User {
        id: number;
        username: string;
        email: string;
        password: string;
        firstName: string;
        lastName: string;
        dateOfBirth: Date;
        address: string;
        phoneNumber: string;
        createdAt: Date;
        updatedAt: Date;
        isActive: boolean;
        role: "admin" | "moderator" | "user";
    }
    
    // Für User-Updates: Alles, außer den vom System verwalteten Feldern
    type T_UserUpdateInput = Omit<I_User, "id" | "createdAt" | "updatedAt">;
    
    // Für User-Erstellung: Ohne ID und Timestamps
    type T_UserCreateInput = Omit<I_User, "id" | "createdAt" | "updatedAt">;
    
    // Für öffentliche Anzeige: Ohne sensitive Daten
    type T_UserSafe = Omit<I_User, "password">;
    
    // Mock-Datenbank
    let usersDatabase: I_User[] = [
        {
            id: 1,
            username: "admin",
            email: "admin@mail.com",
            password: "hashedPassword1",
            firstName: "Admin",
            lastName: "User",
            dateOfBirth: new Date("1985-03-01"),
            address: "Neue Straße 2",
            phoneNumber: "0923 23123 45",
            createdAt: new Date("2025-06-25"),
            updatedAt: new Date("2025-06-25"),
            isActive: true,
            role: "admin"
        },
        {
            id: 2,
            username: "john",
            email: "john@mail.com",
            password: "hashedPassword2",
            firstName: "John",
            lastName: "Doe",
            dateOfBirth: new Date("1990-08-24"),
            address: "Hauptallee 23",
            phoneNumber: "01234 2323 4232",
            createdAt: new Date("2025-06-25"),
            updatedAt: new Date("2025-06-25"),
            isActive: true,
            role: "user"
        }
    ];
    
    /**
    * Generate new ID
    * ---
    * @returns {number}
    */
    function generateNewId(): number {
        return Math.max(...usersDatabase.map(u => u.id)) + 1;
    }
    
    /**
    * Save user to database
    * ---
    * @param {I_User} user - User to save
    * ---
    * @returns {Promise<I_User>}
    */
    async function saveUserToDatabase(user: I_User): Promise<I_User> {
        const existingIndex = usersDatabase.findIndex(u => u.id === user.id);
        
        if (existingIndex >= 0) {
            // Update existing user
            usersDatabase[existingIndex] = user;
        } else {
            // Add new user
            usersDatabase.push(user);
        }
        
        return user;
    }
    
    /**
    * Update user by ID
    * ---
    * @param {number} id - ID of the user
    * @param {Partial<T_UserUpdateInput>} updates - Updated user data
    * ---
    * @returns {Promise<User>}
    */
    async function updateUser(id: number, updates: Partial<T_UserUpdateInput>): Promise<I_User> {
        const userToUpdate = usersDatabase.find(u => u.id === id);
        
        // Prüfe, ob ein Benutzer gefunden wurde
        if (!userToUpdate) {
            throw new Error(`User with ID ${id} not found`);
        }
        
        // ID, createdAt, updatedAt können nicht
        // versehentlich überschrieben werden.
        const updatedUser = {
            ...userToUpdate,
            ...updates,
            id, // ID bleibt unverändert
            updatedAt: new Date() // Wird automatisch gesetzt
        };
        
        return saveUserToDatabase(updatedUser);
    }
    
    /**
    * Create user
    * ---
    * @param {T_UserCreateInput} userData - New user data
    * ---
    * @returns {Promise<I_User>}
    */
    async function createUser(userData: T_UserCreateInput): Promise<I_User> {
        // Validierung - Benutzername
        if (usersDatabase.some(u => u.username === userData.username)) {
            throw new Error("Benutzername bereits vorhanden");
        }
        
        // Validierung - E-Mail
        if (usersDatabase.some(u => u.email === userData.email)) {
            throw new Error("E-Mail bereits vorhanden");
        }
        
        const newUser = {
            ...userData,
            id: generateNewId(),
            createdAt: new Date(),
            updatedAt: new Date()
        };
        
        return saveUserToDatabase(newUser);
    }
    
    /**
    * Get safe user
    * ---
    * @param {I_User} user - User to load
    * ---
    * @returns {T_UserSafe}
    */
    function getUserSafe(user: I_User): T_UserSafe {
        const { password, ...safeUser } = user;
        return safeUser;
    }
    
    async function runExample() {
        try {
            console.log("Initiale Benutzer:", usersDatabase.map(u => getUserSafe(u)));
            
            // 1. Benutzer erstellen
            const newUser = await createUser({
                username: "new_user",
                email: "new@mail.com",
                password: "hashedPassword1",
                firstName: "New",
                lastName: "User",
                dateOfBirth: new Date("1995-05-05"),
                address: "Hauptstraße 3",
                phoneNumber: "1234 999 333",
                isActive: true,
                role: "user"
            });
            
            console.log("--- --- ---");
            console.log("Benutzer erstellt:", getUserSafe(newUser));
            
            // 2. Benutzer aktualisieren
            const updatedUser = await updateUser(2, {
                firstName: "Jonathan",
                address: "Updated St",
                phoneNumber: "555-0999-000"
            });
            console.log("--- --- ---");
            console.log("Aktualisierter Benutzer:", getUserSafe(updatedUser));
            
            // 3. Aktuelle Benutzer anzeigen
            console.log("Aktuelle Benutzer");
            usersDatabase.forEach(user => {
                console.log(getUserSafe(user));
            });
        } catch (error) {
            console.log("Fehler:", error instanceof Error ? error.message : error);
        }
    }
    
    runExample();
    Initiale Benutzer: [
    {
        id: 1,
        username: 'admin',
        email: 'admin@mail.com',
        firstName: 'Admin',
        lastName: 'User',
        dateOfBirth: 1985-03-01T00:00:00.000Z,
        address: 'Neue Straße 2',
        phoneNumber: '0923 23123 45',
        createdAt: 2025-06-25T00:00:00.000Z,
        updatedAt: 2025-06-25T00:00:00.000Z,
        isActive: true,
        role: 'admin'
    },
    {
        id: 2,
        username: 'john',
        email: 'john@mail.com',
        firstName: 'John',
        lastName: 'Doe',
        dateOfBirth: 1990-08-24T00:00:00.000Z,
        address: 'Hauptallee 23',
        phoneNumber: '01234 2323 4232',
        createdAt: 2025-06-25T00:00:00.000Z,
        updatedAt: 2025-06-25T00:00:00.000Z,
        isActive: true,
        role: 'user'
    }
    ]
    --- --- ---
    Benutzer erstellt: {
        username: 'new_user',
        email: 'new@mail.com',
        firstName: 'New',
        lastName: 'User',
        dateOfBirth: 1995-05-05T00:00:00.000Z,
        address: 'Hauptstraße 3',
        phoneNumber: '1234 999 333',
        isActive: true,
        role: 'user',
        id: 3,
        createdAt: 2025-06-25T12:27:16.999Z,
        updatedAt: 2025-06-25T12:27:16.999Z
    }
    --- --- ---
    Aktualisierter Benutzer: {
        id: 2,
        username: 'john',
        email: 'john@mail.com',
        firstName: 'Jonathan',
        lastName: 'Doe',
        dateOfBirth: 1990-08-24T00:00:00.000Z,
        address: 'Updated St',
        phoneNumber: '555-0999-000',
        createdAt: 2025-06-25T00:00:00.000Z,
        updatedAt: 2025-06-25T12:27:17.000Z,
        isActive: true,
        role: 'user'
    }
    Aktuelle Benutzer
    {
        id: 1,
        username: 'admin',
        email: 'admin@mail.com',
        firstName: 'Admin',
        lastName: 'User',
        dateOfBirth: 1985-03-01T00:00:00.000Z,
        address: 'Neue Straße 2',
        phoneNumber: '0923 23123 45',
        createdAt: 2025-06-25T00:00:00.000Z,
        updatedAt: 2025-06-25T00:00:00.000Z,
        isActive: true,
        role: 'admin'
    }
    {
        id: 2,
        username: 'john',
        email: 'john@mail.com',
        firstName: 'Jonathan',
        lastName: 'Doe',
        dateOfBirth: 1990-08-24T00:00:00.000Z,
        address: 'Updated St',
        phoneNumber: '555-0999-000',
        createdAt: 2025-06-25T00:00:00.000Z,
        updatedAt: 2025-06-25T12:27:17.000Z,
        isActive: true,
        role: 'user'
    }
    {
        username: 'new_user',
        email: 'new@mail.com',
        firstName: 'New',
        lastName: 'User',
        dateOfBirth: 1995-05-05T00:00:00.000Z,
        address: 'Hauptstraße 3',
        phoneNumber: '1234 999 333',
        isActive: true,
        role: 'user',
        id: 3,
        createdAt: 2025-06-25T12:27:16.999Z,
        updatedAt: 2025-06-25T12:27:16.999Z
    }

    Record<K, T> - Objekt-Typ mit bestimmten Schlüsseln

    Record<K, T> erstellt einen Objekttyp, dessen Eigenschaftsschlüsseln vom Typ K sind und dessen Eigenschaftswerte vom Typ T sind.

    Syntax
    Record<K extends keyof any, T>
    Implementierung
    type Record<K extends keyof any, T> = {
        [P in K]: T;
    };

    Wie immer, wir fangen wir mit einem einfachen Beispiel an, um auf Anhieb zu sehen, was Record tut.

    Beispiel
    type StringMap = Record<string, string>;
    
    // Äquivalent zum oberen Typ
    type StringMapEquivalent = {
        [key: string]: string;
    };

    Die Objekte dieser Typen müssen Schlüsseln vom Typ String und Werte vom Typ String haben. Erzeugen wir ein paar Beispiel-Objekte mit den beiden (von ihrer Typisierung her identischen) Typen.

    Beispiel 1 (Erweiterung)
    type StringObject = Record<string, string>;
    
    type StringObjectEquivalent = {
        [key: string]: string;
    };
    
    const objectOne = { "one": "100", "two": "200" };
    const objectTwo = { "one": "100", "two": "200" };
    
    console.log(objectOne);
    console.log(objectTwo);
    
    console.log("--- --- ---");
    
    type NumberStringObject = Record<number, string>;
    
    const objectThree = { 1: "One", 2: "Two", 3: "Three" };
    console.log(objectThree);
    { one: '100', two: '200' }
    { one: '100', two: '200' }
    --- --- ---
    { '1': 'One', '2': 'Two', '3': 'Three' }

    Weitere Utility Types

    Exclude<T, U> - Union Typen filtern

    Der Exclude<T, U> Utility Type ist ein nützliches Werkzeug, um bestimmte Typen aus Union-Typen herauszufiltern.

    Implementierung
    type Exclude<T, U> = T extends U ? never : T;

    Diese Definition nutzt Conditional Types und Distributive Conditional Types.

    1. T extends U ? - Prüft, ob der Typ T dem Type U zugeordnet werden kann.
    2. never - Wenn ja, wird der Typ komplett entfernt (never bedeutet “kein Typ”)
    3. T - Wenn nein, bleibt der ursprüngliche Typ erhalten.

    Beispiel

    Werfen wir einen Blick auf das erste Beispiel, um etwas mehr Verständnis zu gewinnen.

    Beispiel
    type T_AllColors = "red" | "green" | "blue" | "yellow" | "orange";
    type T_PrimaryColors = "red" | "green" | "blue";
    
    type T_SecondaryColors = Exclude<T_AllColors, T_PrimaryColors>;
    
    const colorOne: T_AllColors = "red";
    const colorTwo: T_AllColors = "green";
    const colorThree: T_AllColors = "blue";
    const colorFour: T_AllColors = "yellow";
    const colorFive: T_AllColors = "orange";
    
    const primaryColors: T_PrimaryColors[] = [colorOne, colorTwo, colorThree];
    
    // ❌ Funktioniert nicht
    // const primaryColors: T_PrimaryColors[] = [colorOne, colorTwo, "orange"];
    
    const secondaryColors: T_SecondaryColors[] = [colorFour, colorFive];
    
    // ❌ Funktioniert nicht
    // const secondaryColors: T_SecondaryColors[] = ["black", "white"];
    
    console.log("Color one:", colorOne);
    console.log("Color two: ", colorTwo);
    console.log("Color three:", colorThree);
    console.log("Color four:", colorFour);
    console.log("Color five:", colorFive);
    console.log("--- --- ---");
    console.log("Primary colors:", primaryColors);
    console.log("Secondary colors:", secondaryColors);
    Color one: red
    Color two:  green
    Color three: blue
    Color four: yellow
    Color five: orange
    --- --- ---
    Primary colors: [ 'red', 'green', 'blue' ]
    Secondary colors: [ 'yellow', 'orange' ]

    Extract<T, U> - Gemeinsame Typen extrahieren

    Der Extract<T, U> Utility Type ist das Gegenstück zu Exclude<T, U>. Er extrahiert nur die Typen aus T, die auch in U enthalten sind. Es ist wie eine Schnittmenge (Intersection) für Union-Typen.

    Implementierung
    type Extract<T, U> = T extends U ? T : never;

    Diese Definition ist genau umgekehrt zu Exclude.

    1. T extends U ? - Prüft, ob der Typ T dem Typ U zugeordnet werden kann.
    2. T - Wenn ja, bleibt der Typ T erhalten.
    3. never - Wenn nein, wird der Typ entfernt.

    Es filtert also die Typen aus T heraus, die nicht in U vorkommen.

    Ein einfaches Beispiel hierzu.

    Beispiel
    type T_ApiResponse = 
        | { status: "success", data: { id: number; name: string } }
        | { status: "error", message: string }
        | { status: "pending", timeout: number };
    
    // Extrahiere nur Responses mit status="success"
    type T_SuccessResponse = Extract<T_ApiResponse, { status: "success" }>;
    
    /**
    * Process response
    * @param {T_SuccessResponse} response - Response
    */
    function processResponse(response: T_SuccessResponse) {
        console.log("Daten erhalten:", response.data.name);
    }
    
    // Simuliere Response
    const successResponse: T_SuccessResponse = {
        status: "success",
        data: { id: 1, name: "TypeScript" }
    };
    
    const errorResponse: T_ApiResponse = {
        status: "error",
        message: "Fehler"
    };
    
    processResponse(successResponse);
    Daten erhalten: TypeScript

    NonNullable<T> - Null und undefined ausschließen

    Der Type NonNullable<T> entfernt die Typen null und undefined aus einem gegebenen Typ T. Das ist besonders nützlich, wenn man sicherstellen möchte, dass ein Wert niemals null oder undefined sein kann.

    Implementierung
    type NonNullable<T> = T extends null | undefined ? never : T;
    • Für einen Typ T wird geprüft, ob dieser null oder undefined ist.
    • Falls ja, wird never zurückgegeben (was bedeutet, dass der Typ aus der Union entfernt wird).
    • Falls nein, wird der Typ T selbst beibehalten.

    Beispiel

    Betrachten wir ein einfaches Beispiel. In diesem Beispiel haben wir zwei verschiedene Typen. Ein regulärer Typ mit Eigenschaften die null oder undefined sein können und ein Typ, welcher alle Eigenschaften von diesem Basis-Typ übernimmt, außer die, die null oder undefined sein können.

    Beispiel
    type T_UserLight = {
        id: number;
        name: string | null;
        email?: string | undefined;
    };
    
    type T_UserFull = {
        [K in keyof T_UserLight]: NonNullable<T_UserLight[K]>;
    };
    
    // name oder email dürfen nicht gesetzt bleiben
    const userLight: T_UserLight = {
        id: 1,
        name: null,
        email: undefined
    };
    
    // name darf nicht null sein
    // email ist optional, aber nicht undefined
    const userFull: T_UserFull = {
        id: 1,
        name: "John",
        email: "john@mail.com"
    };
    
    console.log(userLight);
    console.log(userFull);
    { id: 1, name: null, email: undefined }
    { id: 1, name: 'John', email: 'john@mail.com' }

    ReturnType<T> - Rückgabetyp einer Funktion extrahieren

    Der Utility Type ReturnType<T> extrahiert den Rückgabetyp einer Funktion oder Methodensignatur. Gut zu gebrauchen, wenn man mit komplexen Funktionen arbeitet oder deren Rückgabetypen wiederverwenden möchte, ohne sie manuell zu kopieren.

    Implementierung
    type ReturnType<T extends (...args: any) => any> =
        T extends (...args: any) => infer R ? R : any;
    1. Generische Einschränkung: T extends (...args: any) => any: Stellt sicher, dass T eine Funktion ist.
    2. Type Inference mit infer: Deklariert einen Platzhalter R für den Rückgabetypen. Wenn T eine Funktion ist, wird R zurückgegeben, sonst any.

    Beispiel 1

    Ein einfaches Beispiel, bei dem der Rückgabetyp einer Funktion extrahiert wird und als Rückgabewert einer anderen Funktion verwendet wird. Einfacher gesagt: Wir haben eine Kern-Funktion, welche einen bestimmten Rückgabetyp hat. Es wird ein Typ definiert, welcher den Rückgabetyp der genannten Funktion hat.

    Beispiel 1
    /**
     * Format currency
     * ----
     * @param {number} amount - Amount (price)
     * @param {string} currency - Currency code
     * ---
     * @returns {string}
    */
    function formatCurrency(amount: number, currency: string): string {
        return new Intl.NumberFormat("de-DE", {
            style: "currency",
            currency
        }).format(amount);
    }
    
    // Extrahiere den Rückgabetyp
    type T_FormattedValue = ReturnType<typeof formatCurrency>; // string
    
    /**
     * Log result (wrapper function)
     * ---
     * @param {number} amount - Amount (price)
     * @param {string} currency - Currency code
     * ---
     * @returns {T_FormattedValue}
    */
    function logResult(amount: number, currency: string): T_FormattedValue {
        const result = formatCurrency(amount, currency);
        console.log("Formatted:", result);
    
        return result;
    }
    
    console.log(logResult(42.99, "EUR"));
    Formatted: 42,99 €
    42,99 €

    Rein technisch könnte man den Rückgabetyp der zweiten Funktion auch manuell platzieren. Hätter allerdings einen Effekt, dass man den Rückgabetyp überall ändern müsste.