Menu

TypeScript Interface: Syntax, optional, Methoden und Beispiele

Ein Interface benennt die Form eines Objekts: welche Eigenschaften es hat und welche Typen sie enthalten. Wie du eines deklarierst, optionale und readonly Eigenschaften, Methoden, Index-Signaturen, Erweitern, Implementieren in einer Klasse, Declaration Merging, generische Interfaces und wie du einem Interface Standardwerte gibst.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Ein Interface gibt der Form eines Objekts einen Namen: den Eigenschaften, die es haben muss, und dem Typ jeder davon. Einmal deklariert, verwendest du den Namen als Typ, und der Compiler prüft jedes Objekt, das du übergibst, zurückgibst oder zuweist, dagegen.

Der letzte Aufruf ist der Compilerfehler TS2741. Interfaces werden beim Kompilieren entfernt: Die JavaScript-Ausgabe hat keine Spur von User, und nichts prüft die Form zur Laufzeit.

Ein Interface deklarieren

Die Syntax ist das Schlüsselwort interface, ein Name (per Konvention in PascalCase) und ein Rumpf, der Member aufführt. Member können durch Semikolons, Kommas oder einfach Zeilenumbrüche getrennt werden; Semikolons sind der übliche Stil.

interface Product {
  sku: string;               // required property
  price: number;
  tags: string[];            // array property
  dimensions: {              // nested object type
    width: number;
    height: number;
  };
  discount?: number;         // optional property
  readonly createdAt: Date;  // cannot be reassigned
  label(): string;           // method
}

Ein Interface ist ein Typ, kein Wert. Es lässt sich nicht mit new instanziieren, es hat keine Standardwerte, und obj instanceof Product ist der Fehler TS2693 ('Product' only refers to a type, but is being used as a value here). Um eine Form zur Laufzeit zu prüfen, schreibe einen Type Guard.

Strukturelle Typisierung und Excess Property Checks

TypeScript vergleicht Formen, keine Namen. Jedes Objekt mit den geforderten Eigenschaften passt zum Interface, egal ob es damit deklariert wurde. Zusätzliche Eigenschaften sind in Ordnung, mit einer Ausnahme: Ein Objektliteral, das direkt dort geschrieben wird, wo das Interface erwartet wird, bekommt einen Excess Property Check, weil ein unbekannter Schlüssel dort fast immer ein Tippfehler ist.

Dieser Fehler (TS2353) fängt { id: 1, name: "a", emial: "x" } für einen User ab: Der Compiler schlägt sogar Did you mean to write 'email'? vor (TS2561).

Optionale und readonly Eigenschaften

Ein ? hinter dem Namen macht eine Eigenschaft optional: Das Objekt darf sie weglassen, und beim Lesen ergibt sich T | undefined. readonly verbietet die Neuzuweisung der Eigenschaft, nachdem das Objekt erzeugt wurde.

Hier zeigen sich zwei Grenzen. Erstens gilt readonly nur beim Kompilieren: Die beiden mit @ts-expect-error markierten Zeilen laufen trotzdem, wenn du auf Run drückst, und sie gelingen, und die letzten Zeilen ändern apiUrl über eine Referenz, die ohne readonly typisiert ist. Zweitens ist es flach: readonly hosts: string[] würde die Neuzuweisung von hosts verhindern, hosts.push(...) aber weiter erlauben, deshalb ist das Array selbst als readonly string[] typisiert. Es dokumentiert die Absicht und setzt sie in typisiertem Code durch; es friert nichts ein. Der Utility Type Readonly<T> macht jede Eigenschaft eines vorhandenen Interfaces auf einmal readonly.

Methoden und Funktionseigenschaften

Eine Methode kann als Methodensignatur geschrieben werden, name(params): ReturnType, oder als Eigenschaft, die eine Funktion enthält, name: (params) => ReturnType. Aufrufer verwenden beide gleich.

Der Unterschied ist subtil: Unter strictFunctionTypes (Teil von strict) werden Parameter von Eigenschaften mit Funktionstyp streng geprüft, Methodensignaturen dagegen lockerer (bivariant), die Eigenschaftsform findet also ein paar Fehler mehr. Die Methodensyntax ist kürzer und der verbreitetere Stil; beides ist in Ordnung.

Ein Interface kann auch etwas Aufrufbares oder Konstruierbares beschreiben, mit einer Call Signature oder einer Construct Signature:

interface Formatter {
  (value: number): string; // call signature: the object is a function
  locale: string;          // and it also has a property
}

interface PointConstructor {
  new (x: number, y: number): { x: number; y: number }; // construct signature
}

Index-Signaturen

Wenn die Eigenschaftsnamen vorher nicht bekannt sind, beschreibt eine Index-Signatur alle auf einmal: [key: string]: T bedeutet „jeder String-Schlüssel, jeder mit einem T“.

Die letzten Zeilen zeigen den Haken: Das Lesen eines nicht existierenden Schlüssels ist als number typisiert, nicht als number | undefined. Die Compileroption noUncheckedIndexedAccess fügt jedem solchen Lesezugriff | undefined hinzu.

Benannte Eigenschaften können neben einer Index-Signatur stehen, müssen aber zu ihr passen. interface Dict { [key: string]: number; name: string } ist der Fehler TS2411, Property 'name' of type 'string' is not assignable to 'string' index type 'number'. Erweitere den Indextyp ([key: string]: number | string) oder verschiebe den dynamischen Teil in eine eigene Eigenschaft. Für einfache Zuordnungen von Schlüsseln zu Werten sagt Record<string, number> dasselbe in einer Zeile.

Ein Interface erweitern

extends baut ein neues Interface aus einem oder mehreren vorhandenen. Das Kind hat jeden Member der Eltern plus seine eigenen:

interface Animal {
  name: string;
}

interface Pet extends Animal {
  owner: string;
}

interface Trained {
  commands: string[];
}

interface ServiceDog extends Pet, Trained {
  certifiedUntil: Date;
}

// ServiceDog requires: name, owner, commands, certifiedUntil

Ein Kind darf eine Eigenschaft der Eltern nur mit einem kompatiblen (engeren) Typ neu deklarieren, etwa kind: "dog", wo die Eltern kind: string sagen. Die Regeln und wie man Typaliasse erweitert, stehen auf der Seite zu extends.

Ein Interface in einer Klasse implementieren

class X implements Shape bittet den Compiler zu prüfen, dass die Klasse alles hat, was das Interface verlangt. Ein fehlender Member ist ein Fehler an der Klassendeklaration:

index.ts(7,7): error TS2420: Class 'Circle' incorrectly implements interface 'Shape'.
  Property 'area' is missing in type 'Circle' but required in type 'Shape'.

Mit ergänztem area() lassen sich mehrere Klassen und sogar ein einfaches Objekt alle als Shape verwenden:

implements ist nur eine Prüfung. Es fügt der Klasse keine Member hinzu und typisiert die Methodenparameter der Klasse nicht für dich: greet(name) {} in einer Klasse, die greet(name: string): string implementiert, ist weiterhin der Fehler TS7006, Parameter 'name' implicitly has an 'any' type. Eine Klasse kann mehrere Interfaces implementieren: class A implements B, C.

Declaration Merging

Wird ein Interface mit demselben Namen zweimal im selben Scope deklariert, verschmelzen beide zu einem. Das können Typaliasse nicht (ein zweites type mit demselben Namen ist ein Fehler wegen doppelten Bezeichners).

interface Settings {
  theme: string;
}

interface Settings {
  fontSize: number;
}

// Settings now requires both properties
const s: Settings = { theme: "dark", fontSize: 14 };

In Anwendungscode ist das selten gewollt, und ein versehentliches Merging kann verwirren. Der echte Nutzen ist, Typen zu ergänzen, die dir nicht gehören: die Optionen einer Bibliothek oder ein Global wie Window. Aus einem Modul heraus packst du die Deklaration in declare global:

declare global {
  interface Window {
    analytics: { track(event: string): void };
  }
}

export {};

Danach besteht window.analytics.track("signup") überall im Projekt die Typprüfung. Pakete mit Typdefinitionen beruhen auf demselben Mechanismus; siehe Deklarationsdateien.

Standardwerte für Interface-Eigenschaften

Ein Interface kann keine Standardwerte enthalten, weil es Typen beschreibt und zur Laufzeit entfernt wird. size?: "sm" | "md" = "md" ist der Fehler TS1246, An interface property cannot have an initializer. Mach die Eigenschaft optional und setze den Standardwert dort ein, wo das Objekt verwendet wird:

Standardwerte beim Destructuring sind die sicherere Wahl: Sie greifen immer, wenn der Wert undefined ist, auch bei einem ausdrücklichen size: undefined. Die Variante mit Spread kopiert dieses ausdrückliche undefined über den Standardwert, und ihr Ergebnis ist trotzdem so typisiert, als wäre size immer gesetzt. Brauchst du diese Garantie aus den Typen, schalte exactOptionalPropertyTypes ein, was size: undefined für ein optionales size?: ... zum Compilerfehler macht. Eine Klasse mit initialisierten Feldern ist die andere Möglichkeit, wenn das Objekt auch Verhalten braucht.

Generische Interfaces

Ein Interface kann Typparameter nehmen, wodurch eine Deklaration für viele Datentypen funktioniert:

interface ApiResponse<T> {
  ok: boolean;
  data: T;
  error?: string;
}

interface Page<T> {
  items: T[];
  nextCursor?: string;
}

interface User {
  id: number;
  name: string;
}

const res: ApiResponse<Page<User>> = {
  ok: true,
  data: { items: [{ id: 1, name: "Ada" }], nextCursor: "abc" },
};

ApiResponse<Page<User>> liest sich als „eine Antwort, deren Daten eine Seite von Usern sind“. Die Standardbibliothek ist voll davon: Array<T>, Promise<T>, Map<K, V> sind alle generische Interfaces.

Interface vs Typalias

Ein Typalias type kann dieselbe Objektform beschreiben, und für einfache Objekttypen sind beide austauschbar. Nur ein Interface kann verschmelzen; nur ein Typalias kann eine Union, ein Tupel oder einen Mapped oder Conditional Type benennen. Die Faustregel des TypeScript-Handbuchs lautet, interface zu verwenden, bis du ein Feature brauchst, das nur type hat. Die Seite interface vs type enthält den vollständigen Vergleich, einschließlich des Unterschieds bei Record<string, ...>, der die meisten überrascht.

Häufig gestellte Fragen

Was ist ein Interface in TypeScript?

Ein Interface ist eine benannte Beschreibung der Form eines Objekts: seine Eigenschaftsnamen, ihre Typen, welche optional oder readonly sind, und seine Methoden. Der Compiler prüft, dass Werte, die als dieses Interface verwendet werden, diese Form haben. Interfaces existieren nur beim Kompilieren; sie erzeugen kein JavaScript.

Wie setze ich einen Standardwert in einem TypeScript-Interface?

Gar nicht: Ein Interface beschreibt Typen, keine Werte, also ist size: "md" = ... keine gültige Syntax. Markiere die Eigenschaft als optional (size?: "sm" | "md") und wende den Standardwert dort an, wo das Objekt verwendet wird, meist mit Standardwerten beim Destructuring in den Funktionsparametern: function render({ size = "md" }: Options). Ein Objekt mit Standardwerten zu spreaden ({ ...DEFAULTS, ...options }) funktioniert auch, aber ein ausdrückliches undefined in options überschreibt den Standardwert.

Wie prüfe ich zur Laufzeit, ob ein Objekt ein Interface implementiert?

Einen eingebauten Weg gibt es nicht, weil Interfaces beim Kompilieren entfernt werden: obj instanceof User ist der Fehler TS2693 ('User' only refers to a type, but is being used as a value here). Schreibe eine Type-Guard-Funktion, die die Eigenschaften prüft, function isUser(x: unknown): x is User { ... }, oder validiere mit einer Schema-Bibliothek.

Kann ein Interface mehrere Interfaces erweitern?

Ja. Liste sie nach extends auf, durch Kommas getrennt: interface ServiceDog extends Pet, Trained { ... }. Das neue Interface hat jeden Member jedes Elternteils plus seine eigenen. Deklarieren zwei Elternteile dieselbe Eigenschaft mit inkompatiblen Typen, ist die Deklaration ein Fehler.

Was ist der Unterschied zwischen einem Interface und einer Klasse in TypeScript?

Eine Klasse existiert zur Laufzeit: Sie hat einen Konstruktor, Methodenimplementierungen, und new erzeugt Objekte daraus. Ein Interface beschreibt nur eine Form für den Compiler und wird aus der JavaScript-Ausgabe entfernt. Eine Klasse kann implements SomeInterface deklarieren, damit der Compiler prüft, dass sie passt, und jedes einfache Objekt mit der richtigen Form passt ebenfalls zum Interface.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S