navigation Navigation


Inhaltsverzeichnis

Interfaces


Interfaces in TypeScript ermöglichen die klare Definition und Strukturierung von Objekttypen. Sie fördern die Wiederverwendbarkeit, Lesbarkeit und Wartbarkeit von Code, indem sie Verträge für die Form von Daten festlegen. Durch den Einsatz von Interfaces lassen sich komplexe Anwendungen typsicher und flexibel gestalten.

Inhaltsverzeichnis

    Einführung

    Interfaces sind das Herzstück von TypeScript’s Typ-System. Sie definieren Contracts (Verträge/Abkommen) für die Struktur von Objekten, Klassen und Funktionen. Im Gegensatz zu Klassen existieren Interfaces nur zur Compile-Zeit und haben keinen Einfluß auf den generierten JavaScript-Code.

    Schlüssel-Eigenschaften

    • Nur zur Compile-Zeit: Interfaces existieren nur während der TypeScript-Entwicklung und werden im finalen JavaScript-Code entfernt. Sie erzeugen keinen Laufzeit-Overhead.
    • Strukturelle Typisierung: TypeScript prüft nicht den Namen des Typs, sondern dessen Struktur (“Duck Typing”).
    • Erweiterbar: Interfaces können durch Vererbung, mehrere Interfaces oder “Declaration Merging” kombiniert werden.
    • Bessere Fehlermeldungen: Bei Abweichungen vom Interface erhält man exakte Compiler-Fehlermeldungen mit Angabe des Problems.
    Grundlegende Syntax
    interface I_Person {
        username: string;
        active: boolean;
        age?: number;
    }

    Erklärung des Beispiels

    Dieses Interface I_Person legt fest, dass ein Person-Objekt mindestens username und active als Pflichtfelder vom Typ string und boolean haben muss. Das Feld age ist optional (?).

    Optionale Properties

    Properties können als optional markiert werden, indem ein ? nach dem Property-Namen gesetzt wird. TypeScript prüft, ob die Property existiert. Fehlt sie, ist das kein Fehler - existiert sie, muss sie zum definierten Typ passen.

    Beispiel
    interface I_UserSettings {
        theme: string;
        fontSize: number;
        darkMode?: boolean;
        notifications?: {
            email: boolean;
            push: boolean;
        }
    }
    
    const settings: I_UserSettings = {
        theme: "dark",
        fontSize: 14
    }

    In diesem Beispiel gilt Folgendes:

    • theme und fontSize sind verpflichtend
    • darkMode und notifications können weggelassen werden

    Readonly Properties

    Properties können als schreibgeschützt markiert werden. Dies ist dann sinnvoll, wenn man sicherstellen möchte, dass bestimmte Werte nicht verändert werden sollen. Eignet sich hervorragend beispielsweise für Konfigurationsobjekte.

    Beispiel
    interface I_ImmutablePoint {
        readonly x: number;
        readonly y: number;
    }
    
    const point: I_ImmutablePoint = { x: 10, y: 20 };

    Man kann beim Erstellen des Objekts die Felder x und y setzen, danach nie wieder ändern. Ein Versuch, diese Werte zu ändern, führt zu einem Compile-Fehler.

    Beispiel (Fortsetzung)
    point.x = 50; // ❌ Cannot assign to 'x' because it is a read-only property.

    Methoden in Interfaces

    Interfaces können Methoden-Signaturen definieren - das heißt, man bestimmt, welche Methoden mit welchem Argument- und Rückgabetyp existieren müssen (oder optional ? existieren können).

    Es gibt zwei Schreibweisen:

    • Methoden-Signatur
    • Property mit Funktionstyp
    Beispiel - Methoden-Signatur
    interface I_Calculator {
        add(a: number, b: number): number;
        subtract(a: number, b: number): number;
        multiply?(a: number, b: number): number;
    }
    
    const basicCalc: I_Calculator = {
        add: (a, b) => a + b,
        subtract: (a, b) => a - b
    }
    
    console.log(basicCalc.add(4, 5));
    console.log(basicCalc.subtract(10, 3));
    9
    7

    Alternative Schreibweise.

    Beispiel - Property mit Funktionstyp
    interface I_Calculator {
        add: (a: number, b: number) => number;
        subtract: (a: number, b: number) => number;
    }

    In den beiden Beispielen müssen die Methoden add und subtract implementiert werden. Die Methode multiply ist optional. Sie kann, muss aber nicht definiert werden.

    Index Signatures

    Mit Index Signatures können Interfaces so definiert werden, dass sie beliebige weitere Properties mit bestimmten Typen zulassen. Das ist für Dictionary-ähnliche Strukturen oder flexible Objekte mit dynamischen Keys.

    Beispiel
    interface I_StringDictionary {
        [key: string]: string;
    }
    
    const sampleDict: I_StringDictionary = {
        name: "John",
        email: "john@mail.com"
    };

    In diesem Beispiel kann das Objekt vom Typ I_StringDictionary beliebige Eigenschaften haben, welche Werte vom Typ string haben müssen.

    Man kann auch eine Mischung aus bekannten Properties (explizit definierte Properties) und dynamischen Properties definieren.

    Beispiel
    interface I_HybridDictionary {
        id: number;
        [key: string]: string | number;
    }
    
    const sampleObject: I_HybridDictionary = {
        id: 1,
        username: "john",
        email: "john@mail.com"
    };

    Interface Vererbung

    Interfaces können andere Interfaces erweitern (extends). Mehrere Interfaces können gleichzeitig erweitert werden (Mehrfachvererbung). Das Kind-Interface erbt alle Properties und Methoden der Eltern.

    Beispiel
    interface I_Animal {
        name: string;
        age: number;
    }
    
    interface I_Dog extends I_Animal {
        breed: string;
        bark(): void;
    }
    
    const myDog: I_Dog = {
        name: "Rex",
        age: 3,
        breed: "Labrador",
        bark: () => console.log("Woof")
    };

    Hier ist ein Beispiel für Mehrfachvererbung.

    Beispiel - Mehrfachvererbung
    interface I_Identifiable {
        id: string;
    }
    
    interface I_Timestamped {
        createdAt: Date;
    }
    
    interface I_User extends I_Identifiable, I_Timestamped {
        username: string;
    }
    
    const userOne: I_User = {
        id: 1,
        createdAt: new Date(),
        username: "John"
    };

    Funktions-Interfaces

    Interfaces können Funktionstypen beschreiben. Das ist praktisch, um Funktionsobjekte mit vordefinierter Signatur zu typisieren.

    Beispiel
    interface I_SearchFunction {
        (source: string, subString: string): boolean;
    }
    
    const mySearch: I_SearchFunction = (src, sub) => {
        return src.includes(sub);
    };

    Das Interface I_SearchFunction beschreibt die Signatur: Zwei Strings als Parameter, Rückgabewert muss ein Boolean sein. Eine zugewiesene Funktion muss genau dieser Signatur entsprechen.

    Interfaces mit Klassen

    Eine Klasse kann ein oder mehrere Interfaces implementieren (implements). Die Klasse muss alle Eigenschaften und Methoden aus dem Interface bereitstellen.

    Beispiel
    interface I_ClockInterface {
        currentTime: Date;
        setTime(d: Date): void;
    }
    
    class Clock implements I_ClockInterface {
        currentTime: Date = new Date();
    
        setTime(d: Date): void {
            this.currentTime = d;
        }
    
        anotherFunc(): void {
            console.log("Do nothing");
        }
    }
    
    const clockObject = new Clock();
    clockObject.setTime(new Date());
    clockObject.anotherFunc();

    Die Klasse Clock garantiert, dass sie currentTime als Property und die Methode setTime wie im Interface besitzt. Jede Abweichung führt zu einem Typ-Fehler.

    Generische Interfaces

    Interfaces können generisch sein, also Typ-Parameter akzeptieren (<T>). Damit werden Interfaces sehr flexibel und können für viele Typen wiederverwendet werden.

    Beispiel
    interface I_User {
        id: string;
        name: string;
        email: string;
    }
    
    interface I_Repository<T> {
        findById(id: string): T | undefined;
        save(entity: T): void;
        delete(id: string): boolean;
    }
    
    class UserRepository implements I_Repository<I_User> {
    
        private users: I_User[] = [];
    
        findById(id: string): I_User | undefined {
            return this.users.find(user => user.id === id);
        }
    
        save(user: I_User): void {
            const existingUserIndex = this.users.findIndex(u => u.id === user.id);
            
            if (existingUserIndex >= 0) {
                // Aktualisiere den Benutzer
                this.users[existingUserIndex] = user;
            } else {
                // Neuen Benutzer hinzufügen
                this.users.push(user);
            }
        }
    
        delete(id: string): boolean {
            const initialLength = this.users.length;
            this.users = this.users.filter(u => u.id !== id);
            return this.users.length !== initialLength;
        }
    
    
        // Zusätzliche spezifische Methode für UserRepository
        findByEmail(email: string): I_User | undefined {
            return this.users.find(user => user.email === email);
        }
    
        getAllUsers(): I_User[] {
            return this.users;
        }
    
    }
    
    const userRepository = new UserRepository();
    
    userRepository.save({
        id: "1",
        name: "John Doe",
        email: "john@mail.com"
    });
    
    userRepository.save({
        id: "2",
        name: "Alice Brown",
        email: "alice@mail.com"
    });
    
    // Benutzer suchen
    const userOne = userRepository.findById("1");
    console.log(userOne);
    
    // Benutzer löschen
    const isUserDeleted = userRepository.delete("2");
    console.log(isUserDeleted);
    
    // Nicht existierenden Benutzer suchen
    const userThree = userRepository.findById("3");
    console.log(userThree);
    { id: '1', name: 'John Doe', email: 'john@mail.com' }
    true
    undefined
    

    Das Interface I_Repository<T> arbeitet mit einem Platzharlter T. Die Klasse UserRepository implementiert das Interface für den konkreten Typ I_User.

    Declaration Merging

    TypeScript erlaubt es, mehrere Interface-Definitionen mit demselben Namen zu verschmelzen. Die Properties werden zusammengeführt.

    Beispiel
    interface I_User {
        name: string;
    }
    
    interface I_User {
        age: number;
    }
    
    const user: I_User = {
        name: "John",
        age: 30
    };

    Beide Interface-Definitionen werden zu einem einzigen Interface zusammengeführt. Das ist nützlich für Erweiterungen in verschiedenen Modulen oder Bibliotheken (z.B. globale Typen, Frameworks). Sprich, wenn bereits einige Definitionen vorhanden sind und man diese mit eigenen ergänzen möchte.