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
        }
    ]