navigation Navigation


Inhaltsverzeichnis

Komplexe Typen


Komplexe Datentypen in TypeScript ermöglichen die präzise Modellierung vielfältiger Datenstrukturen und fördern die Typsicherheit in modernen Anwendungen. Durch den Einsatz von Arrays, Tupeln, Enums, Objekten sowie Union- und Intersection-Types lassen sich flexible und dennoch klar definierte Schnittstellen gestalten. Diese Typen unterstützen die Entwicklung skalierbarer, wartbarer und fehlerarmer Software, indem sie die Vorteile der statischen Typisierung gezielt auf komplexe Anwendungsfälle ausweiten.

Inhaltsverzeichnis

    Arrays

    Arrays sind eine der häufigsten Datenstrukturen. TypeScript bietet mehrere Wege, Arrays zu typisieren und sicherzustellen, dass sie nur die erwarteten Datentypen enthalten.

    Array-Typ Syntax

    Die Syntax für die Typisierung von Array sieht wie folgt aus.

    let nums1: number[] = [1, 2, 3, 4, 5];
    let nums2: Array<number> = [1, 2, 3, 4, 5];

    Hier haben wir zwei gleichwertige Schreibweisen. Die erste Syntax (T[]) ist häufiger und kompakter. Die zweite Syntax (Array<T>) ist nützlich bei komplexen Typen.


    Wichtigkeit von Typisierung

    Warum ist eigentlich die Typisierung so wichtig? Wenn man sich ein Beispiel von einem Array in JavaScript anschaut, wird man feststellen, dass es durchaus möglich ist, Fehler zu produzieren, welche nicht sofort, sondern erst zur Laufzeit sich auswirken.

    JavaScript Version
    const prices = [10.99, 24.50, 5.00];
    prices.push("29.99");
    const total = prices.reduce((sum, price) => sum + price, 0);
    
    console.log(total);
    console.log(typeof total);
    '40.4929.99'
    'string'

    Wie wir an diesem Beispiel sehen, konnten wir ohne Fehler einen String statt einer Zahl dem prices Array hinzufügen. Das führt dazu, dass das Endergebnis falsch ist. Zusätzlich erhalten wir ein Ergebnis als ein String und nicht als eine Zahl.

    Mit TypeScript würden wir diesen Fehler beim Kompilieren gemeldet kriegen.

    TypeScript Version
    const prices: number[] = [10.99, 24.50, 5.00];
    prices.push("29.99"); // Error: Argument of type 'string' is not assignable
    const total = prices.reduce((sum, price) => sum + price, 0);

    Damit haben wir sichergestellt, dass nur Werte vom Typ number im prices Array enthalten sein können.


    Array-Methoden und Type-Safety

    TypeScript versteht, wie Array-Methoden Typen transformieren.

    Typ-Transformation - map

    Die Methode .map() erzeugt aus jedem Array-Element einen neuen Wert. Das Resultat kann einen anderen Typ als das Ursprungs-Array haben.

    const nums: number[] = [1, 2, 3, 4, 5];
    
    // Wandelt number => string
    const strings: string[] = nums.map(n => n.toString());
    
    // Typ bleibt gleich
    const doubled: number[] = nums.map(n => n * 2);

    TypeScript erkennt automatisch den Rückgabetyp der Mapping-Funktion und erstellt daraus das Ergebnis-Array. So entsteht Type Safety: Das Ergebnis ist immer ein Array vom Typ des Rückgabewerts.

    // bools ist vom Typ boolean[]
    const bools = nums.map(n => n > 2);

    Typ-Verfeinerung - filter

    Die .filter() Methode erstellt ein Teil-Array mit nur den Elementen, die den Filter passieren.

    const nums: number[] = [1, 2, 3, 4, 5];
    const evenNums = nums.filter(n => n % 2 === 0);

    Da das Ursprungsarray nur number enthält, bleibt auch das Ergebnis number[].

    Manchmal hat man allerdings ein Array mit mehreren Typen. Hier ein Beispiel.

    Array mit gemischten Typen
    const mixed: (string | number)[] = [1, "Hello", 2, "world"];

    Wenn man nun aus diesem Array nur Strings extrahieren wollen würde, könnte man sich eine Hilfs-Funktion schreiben, die eine spezielle Signatur verwendet.

    Beispiel (Ausbau)
    const mixed: (string | number)[] = [1, "Hello", 2, "world"];
    
    function isString(value: unknown): value is string {
        return typeof value === "string";
    }
    
    const onlyStrings = mixed.filter(isString);
    console.log(onlyStrings);

    Die spezielle Signatur (Type Predicate) value is string sagt TypeScript: “Wenn diese Funktion true liefert, dann ist der Wert garantiert ein String”.

    TypeScript erkennt, dass das Resultat nur noch Strings enthält - ohne zusätzlichen Cast oder Assertion.

    Ohne Type Predicate würde TypeScript nicht erkennen, dass das Filter-Ergebnis ein reines String-Array ist, sondern (string | number)[] als Wert annehmen.

    Werte Aggregation - reduce

    Die Methode .reduce() berechnet einen einzelnen Wert aus allen Array-Elementen.

    Mit der Angabe eines Startwerts für den Akkumulator, erkennt TypeScript den Typ des Ergebnisses.

    Beispiel
    const nums: number[] = [1, 2, 3, 4, 5];
    
    const sum = nums.reduce((acc, cur) => acc + cur, 0);
    console.log(typeof sum);
    console.log(sum);
    
    const concatenated = nums.reduce((acc, cur) => acc + cur.toString(), "");
    console.log(typeof concatenated);
    console.log(concatenated);
    number
    15
    string
    12345

    Mehrdimensionale Arrays

    Ein mehrdimensionales Array ist ein Array, dessen Elemente wiederum Arrays sind.

    Typnotation in TypeScript

    • number[] ist ein eindimensionales Array von Zahlen
    • number[][] ist ein zweidimensionales Array - jedes Element ist wiederum ein number[]
    • number[][][] ist ein dreidimensionales Array

    Hier ein Beispiel für die Deklaration und Initialisierung eines 2D-Arrays (Matrix).

    Beispiel - 2D-Array
    let matrix: number[][] = [
        [1, 2, 3],
        [4, 5, 6],
        [7, 8, 9]
    ];

    Auf Elemente dieses Arrays würde man mit zwei Indizes zugreifen.

    Zugriff auf Element (2D-Array)
    // matrix[zeile][spalte]
    let element = matrix[1][2];
    console.log(element); // 6

    Nun ein Beispiel für die Deklaration und Initialisierung eines 3D-Arrays.

    Beispiel - 3D-Array
    let cube: number[][][] = [
        [
            [1, 2],
            [3, 4]
        ],
        [
            [5, 6],
            [7, 8]
        ]
    ];

    Um bei einem 3D-Array auf ein Element zuzugreifen, würde man mit drei Indizes arbeiten.

    Zugriff auf Element (3D-Array)
    // cube[ebene][zeile][spalte]
    let value = cube[1][0][1];
    console.log(value); // 6

    ReadonlyArray - Unveränderliche Arrays

    Ein ReadonlyArray ist ein Array, das nicht verändert werden kann.

    Das beudetet:

    • keine Elemente hinzufügen
    • keine Elemente entfernen
    • keine Elemente verändern

    Der Hauptnutzen besteht darin, die unbeabsichtigte Änderungen zu verhindern. Gerade in großen Anwendungen und bei der Übergabe von Arrays an Funktionen.

    Syntax

    Klassische Schreibweise: ReadonlyArray<T>

    let readonlyNums: ReadonlyArray<number> = [1, 2, 3];

    Kurzschreibweise: readonly T[]

    let readonlyStrings: readonly string[] = ["a", "b", "c"];

    Methoden

    Bei ReadonlyArray sind nur die Methoden erlaubt, welche das Array nicht verändern.

    Erlaubte Methoden: slice(), map(), filter(), reduce(), forEach(), includes(), find().

    Beispiel
    const upper = readonlyStrings.map(str => str.toUpperCase());

    Verbotene Methoden: push(), pop(), shift(), unshift(), splice(), sort(), direkter Zugriff arr[0] = ....

    Beispiel
    readonlyStrings.push("d"); // ❌ Error
    readonlyStrings.sort(); // ❌ Error

    Praktisches Beispiel - Funktionsparameter schützen

    Warum könnte es von Bedeutung sein? Wenn man Arrays an Funktionen übergibt, will man oft verhindern, dass sie innerhalb der Funktion verändert werden.

    Beispiel
    function processItems(items: readonly string[]): string {
        // items.sort(); ❌ Fehler. sort() verändert das Array.
    
        const sorted = [...items].sort(); // ✅ Neue Kopie
        return sorted.join(", ");
    }

    In diesem Beispiel wurde items als readonly string[] deklariert. Somit sind alle Methoden, die items verändern würden, nicht erlaubt. Nur durch Erstellung einer Kopie können neue oder veränderte Strukturen erzeugt werden.


    Deep Readonly

    Es besteht die Möglichkeit mit as const Literal-Feature das Array und die Objekte darin so tief wie möglich readonly zu setzen.

    Hier ein Beispiel, welches beide Möglichkeiten aufzeigt. Einmal die klassische, manuelle Zuweisung von readonly für jede Property und jeden Wert und einmal die Verwendung von as const.

    Beispiel
    const CONFIG_ONE: readonly [
        { readonly name: "debug", readonly value: true },
        { readonly name: "timeout", readonly value: 3000 }
    ] = [
        { name: "debug", value: true },
        { name: "timeout", value: 3000 }
    ];
    
    // error TS2540: Cannot assign to 'name' because it is a read-only property.
    CONFIG_ONE[0].name = "New value one";
    
    
    const CONFIG_TWO = [
        { name: "debug", value: true },
        { name: "timeout", value: 3000 }
    ] as const;
    
    //  error TS2540: Cannot assign to 'name' because it is a read-only property.
    CONFIG_TWO[0].name = "New value two";

    Zu beachten ist, dass in JavaScript, wenn man diesen Code ohne Modifikationsversuch kompiliert, die Values dennoch veränderlich sind.

    Tuples

    Tuples sind Arrays mit einer festen Anzahl von Elementen spezifischer Typen in einer bestimmten Reihenfolge. Sie sind perfekt für Daten, die zusammengehören, aber unterschiedliche Typen haben.

    Hier ein Beispiel.

    Beispiel
    // Zwei Zahlen, fester Typ und Reihenfolge
    let point: [number, number] = [10, 20];
    
    // String, Zahl, Boolean
    let user: [string, number, boolean] = ["John", 30, true];

    Der Unterschied zu einem Array ist, dass ein Array z.B. einfach number[] oder string[] ist - alle Elemente haben denselben Typ, wobei die Länge variieren kann.

    Ein Tuple ist typsicher, kompakt und für strukturierte, kleine Datensätze mit unterschiedlicher Typenfolge ideal.

    Zugriff auf Tuple-Elemente

    Der TypeScript-Compiler gibt Fehler aus, wenn man auf nicht-existente Tuple-Positionen zugreift.

    Beispiel
    const point: [number, number] = [10, 20];
    
    const x = point[0]; // Typ: number
    const y = point[1]; // Typ: number
    
    const z = point[2]; // ❌ Fehler

    Destructing (Entpacken)

    Das Entpacken funktioniert wie beim Array. Wichtig ist es darauf zu achten, dass die Reihenfolge korrekt beachtet wird.

    Beispiel
    let user: [string, number, boolean] = ["John", 30, true];
    
    const [name, age, isActive] = user;
    
    console.log(name);
    console.log(age);
    console.log(isActive);
    John
    30
    true

    Vorteile von Tuples

    Es gibt ein paar gewichtige Vorteile, warum man in bestimmten Situationen Tuples verwenden sollte.

    Mehr Typsicherheit und Lesbarkeit

    Ohne Tuples
    function getCoordinatesObject(): { x: number, y: number } {
        return { x: 10, y: 20 };
    }

    Der Rückgabewert in diesem Beispiel (ohne Tuples) ist ein Objekt. Man greift mit Namen auf die Felder zu.

    Mit Tuples
    function getCoordinatesObject(): [number, number] {
        return [10, 20];
    }

    Hier ist der Rückgabewert ein kompaktes Datenpaket in fester Reihenfolge.


    Mehrere Rückgabewerte

    Mit Hilfe von Tuples kann man für eine Funktion mehrere Rückgabewerte definieren.

    Beispiel
    function divMod(dividend: number, divisor: number): [number, number] {
        return [Math.floor(dividend / divisor), dividend % divisor];
    }
    
    const [quotient, remainder] = divMod(10, 3);
    
    console.log(quotient); // 3
    console.log(remainder); // 1

    Man kann mehrere Werte kompakt und typisiert zurückgeben.

    Benannte Tuples

    Man kann Tuples mithilfe von type benennen.

    Beispiel
    type Point3D = [x: number, y: number, z: number];
    type UserRecord = [
        id: string,
        name: string,
        age: number,
        isActive: boolean
    ];

    Man soll beachten, dass die Namen (x, y, z) nur für Dokumentation und IDEs sind - in JavaScript bleiben Tuples positionsbasiert.

    Rest-Elemente

    Die Rest-Elemente in Tuples erlauben es Tuples mit einer variablen Länge von Elementen nach einer festen Reihenfolge (dem festen Part) zu definieren.

    Beispiel
    type StringNumberBooleans = [string, number, ...boolean[]];
    
    const data1: StringNumberBooleans = ["Hello", 42]; // ✅ Ok
    const data2: StringNumberBooleans = ["Hello", 42, true]; // ✅ Ok
    const data3: StringNumberBooleans = ["Hello", 42, true, false]; // ✅ Ok

    Die ersten beiden Elemente sind immer ein string und ein number, danach können beliebig viele Booleans folgen.

    Hier ein praktisches Beispiel dazu.

    Beispiel
    type LogEntry = [
        severity: "info" | "warn" | "error",
        message: string,
        ...data: any[]
    ];
    
    function log(...entry: LogEntry) {
        const [severity, message, ...data] = entry;
        console[severity](message, ...data);
    }
    
    log("info", "User logged in", { userId: 123 });
    log("error", "Failed to connect", ["Fehler 1", "Fehler 2"], { retry : 3 });
    User logged in { userId: 123 }
    Failed to connect [ 'Fehler 1', 'Fehler 2' ] { retry: 3 }

    Readonly Tuples

    Mit readonly kann man die Werte vor Veränderungen schützen.

    Möglichkeit 1 - readonly

    Beispiel
    let readonlyPoint: readonly [number, number] = [10, 20];
    
    // Änderungsversuch
    readonlyPoint[0] = 30; // ❌ Error: TS2540: Cannot assign to '0' because it is a read-only property.

    Möglichkeit 2 - Const Assertion

    Mit as const wird aus einem normalen Array ein readonly Tuple mit Literal-Typen. Die Werte sind nicht nur readonly, sondern behalten ihren exakten Wert.

    Syntax (Schema)
    let point = [10, 20] as const;

    Hier ein praxisbezogenes Beispiel.

    Beispiel
    const DATABASE_CONFIG = [
        "localhost",
        5432,
        "my_database",
        "my_user",
        "my_password"
    ] as const;
    
    type DatabaseConfigType = typeof DATABASE_CONFIG;
    console.log(DatabaseConfigType); // object
    
    DATABASE_CONFIG[0] = "127.0.0.1"; // Fehler: TS2540: Cannot assign to '0' because it is a read-only property.

    Objekte

    Objekte sind das Herzstück von JavaScript und TypeScript. Das Typsystem bietet mächtige Werkzeuge, um Objekt-Strukturen präzise zu beschreiben.

    In TypeScript kann man den Typ von Objekten explizit beschreiben. Das bedeutet, man sagt TypeScript, wie die Struktur eines Objekts aussieht - welche Properties es gibt und welchen Typ sie jeweils haben.

    Damit bekommt man:

    • Type Safety: Fehler werden frühzeitig erkannt.
    • Autocomplete & IntelliSence: Der Editor kann bessere Unterstützung liefern.
    • Dokumentation: Der Code ist besser lesbar und verständlicher.

    Inline-Objekttypen

    Hier ist die schnellste und direkteste Art, einen Objekttyp festzulegen.

    Beispiel
    let user = {
        name: string,
        age: number,
        email: string
    } = {
        name: "John",
        age: 30,
        email: "john@example.com"
    }

    Erklärung

    user ist vom Typ Objekt mit drei Eigenschaften.

    • name: Typ = string
    • age: Typ = number
    • email: Typ = string

    Wenn man z.B. eine Property vergisst, falsche schreibt oder den falschen Typ verwendet, zeigt TypeScript einen Fehler an.

    Beispiel mit Fehler
    let user: {
        name: string;
        age: number;
        email: string
    } = {
        name: "John",
        age: "dreißig", // ❌ Error: string ist nicht erlaubt, number wird erwartet
        email: "john@mail.com"
    };

    Optionale Properties (?)

    Mit dem Fragezeichen hinter einer Property kann man sie optional machen. Sie kann gesetzt sein - muss aber nicht.

    Beispiel
    let product: {
        id: string;
        name: string;
        description?: string; // Optional
        price: number
    } = {
        id: "p12345",
        name: "TypeScript Buch",
        price: 29.99
    };

    In diesem Beispiel drüber ist description optional. Beim Erstellen eines Objektes kann diese Eigenschaft fehlen. Wenn sie allerdings vorhanden ist, muss der Typ ein String sein.


    Readonly Properties

    Das readonly Schlüsselwort macht eine Property schreibgeschützt. Man kann sie nur beim Erstellen des Objekts setzen, danach aber nicht mehr verändern.

    Beispiel
    let config: {
        readonly apiUrl: string;
        readonly maxRetries: number;
        timeout: number;
    } = {
        apiUrl: "https://api.example.com",
        maxRetries: 3,
        timeout: 5000
    };
    
    config.timeout = 10000; // ✅ Ok - möglich
    config.apiUrl = "new-url"; // ❌ Fehler - nicht zulässig

    Das Schlüsselwort readonly macht die Eigenschaft “immutable” (unveränderlich).


    Index Signaturen

    Index Signaturen erlaubt es, Objekttypen mit beliebigen, dynamischen Properties (Eigenschaften) zu definieren, deren Namen und Anzahl zur Entwicklungszeit noch nicht bekannt sind.

    String Index Signatures - das klassische Dictionary

    interface StringDictionary {
        [key: string]: string;
    }

    Was passiert hier?

    • [key: string] ist die Index Signatur
    • Sie erlaubt beliebige Strings als Property-Namen
    • Jeder Wert dieser Eigenschaft muss vom Typ string sein
    Beispiel
    const translations: StringDictionary = {
        hello: "Hallo",
        goodbye: "Auf Wiedersehen"
    };

    Number Index Signatures - Arrays und Pseudo-Arrays

    Number-Index-Signaturen erben die String-Index-Signaturen Regel, da JavaScript alle Objekt-Keys letztlich als Strings behandelt.

    interface NumberArray {
        [index: number]: string;
        length: number;
    }

    Erklärung

    • [index: number] erlaubt beliebige Zahlen als Index
    • Diese Zahlen stehen meist für Array-Indizes
    • Jeder Wert muss vom Typ string sein
    • Es können auch feste Properties wie length vergeben werden

    Gemischte Index Signatures und strukturierte Typen

    Es ist möglich etwas dynamischere Objekt-Interfaces zu definieren, die sowohl feste als auch dynamische Typisierung beinhalten.

    Beispiel
    interface FlexibleData {
        id: number; // Feste Typisierung
        name: string; // Feste Typisierung
        [key: string]: any; // Erlaubt weitere beliebige Eigenschaften
    }

    Die Index Signatur [key: string]: any erweitert den Typ so, dass beliebige weitere Eigenschaften möglich sind.

    Hier ein weiteres Beispiel mit echtem Objekt.

    Beispiel
    const data: FlexibleData = {
        id: 1,
        name: "John",
        extra: "allowed",
        another: 123,
        nestd: { deep: true }
    };

    Wichtiger Hinweis: Die Typen der festen Eigenschaften müssen mindestens mit der Index Signatur kompatibel sein.

    interface Broken {
        id: number; // ❌ Konflikt (Fehler)
        [key: string]: string;
    }

    In diesem Beispiel drüber müssen alle Eigenschaften eines künftigen Objekts, welches das Interface Broken implementiert, vom Typ string sein. Auch die explizit angegebenen. Hier versuchten wir eine Eigenschaft vom number zu definieren. Das stellt einen Konflikt dar.

    Eine mögliche Lösung wäre in diesem Fall entweder die Erweiterung auf any oder auf string | number.

    interface Correct {
        id: number; // ✅ Funktioniert
        [key: string]: string | number;
    }

    Unterschied: String Index vs. Number Index Signatures

    Es kann für die korrekte Definition von Objekt-Interfaces von Bedeutung sein, den Unterschied zwischen String- und Index-Signaturen zu verstehen.

    String-Index

    Definition: [key: string]

    • Betrifft alle Eigenschaften des Objekts
    • Alle Eigenschaften müssen dem angegebenen Typ entsprechen
    • In JavaScript werden alle Property-Keys letztendlich als Strings behandelt
    interface InterfaceOne {
        id: number; // ❌ Konflikt
        [key: string]: string;
    }

    Number-Index

    Definition: [key: number]

    • Betrifft nur numerische Indizes
    • Benannte Eigenschaften sind davon ausgenommen
    • Modelliert Array-ähnliche Strukturen
    interface InterfaceTwo {
        [index: number]: string | undefined;
        length: number; // ✅ Funktioniert
    }

    Verschachtelte Objekte

    In TypeScript können Objekt weitere Objekte oder Arrays als Properties (Eigenschaften) besitzen - also Verschachtelungen beliebiger Tiefe sind möglich.

    Einfache Objekttypen

    Hier ist Address ein einfaches Objekt mit vier String-Eigenschaften.

    interface Address {
        street: string;
        city: string;
        zipCode: string;
        country: string;
    }

    Verschachtelte Objekte (Nested Objects)

    Objekte können Properties besitzen, die wiederum Objekte sind, entweder als eigene Interfaces oder Inline-Typen.

    interface Address {
        street: string;
        city: string;
        zipCode: string;
        country: string;
    }
    
    interface Company {
        name: string;
        address: Address; // Nested object
    }

    Die Eigenschaft address vom Typ Address verweist auf ein weiteres Interface.

    Man kann die Definition noch eine Ebene tiefer ausbauen.

    interface Address {
        street: string;
        city: string;
        zipCode: string;
        country: string;
    }
    
    interface Company {
        name: string;
        address: Address; // Verschachteltes Objekt
    }
    
    interface Person {
        name: string;
        age: number;
        address: Address;
        employer?: Company; // Optionales verschachteltes Objekt
    }

    Hier ein etwas ausführlicheres Beispiel mit einem Praxisbezug. Zuerst definieren wir das Interface.

    Beispiel: Interface
    interface ApiResponse<T> {
        data: T;
        meta: {
            timestamp: string;
            version: string;
            pagination?: {
                page: number;
                pageSize: number;
                total: number;
            };
        };
        errors?: Array<{
            code: string;
            message: string;
            field?: string;
        }>
    }

    Erklärung

    • Generics (<T>): Das Datenfeld ist flexibel - die Struktur hängt vom Anwendungsfall ab
    • meta: Enthält Informationen zur Antwort (z.B. Pagination) und kann selbst verschachtelte Objekte besitzen
    • errors: Ein Array von Fehlerobjekten mit weiteren optionalen Feldern

    Nun wenden wir dieses Interface an.

    Beispiel: Anwendung
    const response: ApiResponse<Person> = {
        data: {
            name: "John",
            age: 30,
            address: {
                street: "Musterstraße 1",
                city: "Berlin",
                zipCode: "10115",
                country: "DE"
            },
            employer: {
                name: "Tech GmbH",
                address: {
                    street: "Fabrikweg 3",
                    city: "Hamburg",
                    zipCode: "20095",
                    country: "DE"
                }
            }
        },
        meta: {
            timestamp: "2024-06-21T22:23:00Z",
            version: "1.2.0",
            pagination: {
                page: 1,
                pageSize: 10,
                total: 50
            }
        },
        errors: [
            { code: "NOT_FOUND", message: "Keine weiteren Daten" }
        ]
    };

    Best Practices für komplexe Typen

    Verwendung von Interfaces für Objekte

    // ✅ Gut: Interface für Objekt-Strukturen
    interface User {
        id: number;
        name: string;
    }
    
    // ❌ Weniger gut: Type Alias für einfache Objekte
    type User = {
        id: number;
        name: string;
    };

    Readonly für Unveränderlichkeit

    Man sollte nach Möglichkeit Daten vor unbeabsichtigten Änderungen schützen.

    function processUser(user: Readonly<User>) {
        // user.name = "New name"; // Error
    }
    
    // Für Änderungen Kopien erstellen
    return { ...user, name: user.name.toUpperCase() };

    Präzise Array-Typen

    // Zu allgemein
    let items: any[] = [];
    
    // Besser: Spezifischer Typ
    let userIds: string[] = [];
    
    // Am besten: Readonly wenn möglich
    const CONFIG_FLAGS: readonly string[] = ["debug", "verbose"];

    Tuples für zusammengehörige Daten

    // ❌ Mehrere separate Variablen
    let lat = 52.520008;
    let lng = 13.404954;
    
    // ✅ Tuple für zusammengehörige Werte
    let berlinCoords: [latitude: number, longitude: number] = [52.520008, 13.404954];