Mit extends vererbt ein Interface die Properties eines anderen — Single- oder Multiple-Inheritance, sauber per Schlüsselwort statt per Ampersand. Mit implements verpflichtet sich eine Klasse, alle Member eines Interfaces bereitzustellen — der Compiler prüft den Vertrag bei jeder Methode und jedem Feld. Beide Mechanismen wirken auf den ersten Blick verwandt, lösen aber unterschiedliche Probleme: extends baut Typ-Hierarchien, implements koppelt eine konkrete Klasse an einen abstrakten Vertrag. Dieser Artikel zeigt beide im Detail, vergleicht interface extends mit Intersection-Types, und beleuchtet das oft übersehene Detail, dass implements den Klassen-Typ nicht auf den Interface-Typ verengt. Du lernst zusätzlich die Stolperfallen kennen: Property-Konflikte bei Mehrfach-Vererbung, optionale Member, die nicht automatisch entstehen, sowie die Grenze zwischen TS-only-Keyword und JS-Output.
Interface extends Interface
Die häufigste Form der Wiederverwendung auf Typebene ist interface B extends A: das neue Interface erbt alle Properties des Eltern-Interfaces und kann zusätzlich eigene Member ergänzen. Strukturell ist das identisch dazu, alle Properties manuell zu wiederholen — semantisch dokumentiert es aber eine Verwandtschaft.
interface Animal {
name: string;
// jede Tier-Art kann sich bewegen — Pflicht-Methode.
move(): void;
}
// Dog hat ALLES von Animal plus eine eigene Methode.
interface Dog extends Animal {
bark(): void;
}
// Konsument muss alle drei Member liefern.
const rex: Dog = {
name: "Rex",
move() { console.log("trabt los"); },
bark() { console.log("wuff"); },
};Wichtig zu verstehen: extends zwischen Interfaces ist kein Runtime-Konzept. Es entsteht keine Prototypen-Kette, kein JS-Code, kein Laufzeit-Bezug zwischen Dog und Animal. Es ist eine reine Compile-Time-Beziehung — der Compiler kopiert die Properties intern in den Sub-Typ und prüft sie dann gegen jeden Wert, der als Dog deklariert ist. Im transpilierten JavaScript taucht weder Animal noch Dog auf, denn Interfaces existieren ausschließlich im Typsystem.
Mehrfach-Extends
Anders als Klassen in JavaScript dürfen Interfaces in TypeScript mehrere Eltern haben. Die Syntax: kommaseparierte Liste hinter extends. Das Sub-Interface erbt die Vereinigung aller Properties.
interface Named {
name: string;
}
interface Aged {
age: number;
}
interface Located {
country: string;
}
// Person vereint drei orthogonale Aspekte zu einem Typ.
interface Person extends Named, Aged, Located {
// Eigene Properties sind weiterhin erlaubt.
email: string;
}
const anna: Person = {
name: "Anna",
age: 34,
country: "DE",
email: "anna@example.com",
};Mehrfach-Vererbung bei Interfaces ist unproblematisch, weil Interfaces keine Implementierungen tragen — es gibt kein Diamond-Problem wie in C++ oder anderen klassischen OO-Sprachen. Es werden lediglich Struktur-Beschreibungen vereint. Genau diese Vereinigung kann allerdings zu Konflikten führen, wenn zwei Eltern dieselbe Property mit unterschiedlichen Typen tragen — Thema des nächsten Abschnitts.
Property-Konflikte beim extends
Was passiert, wenn A ein name: string deklariert und B ein name: number? Bei interface C extends A, B schlägt der Compiler sofort Alarm — und das ist gut so. Der gleiche Konflikt würde bei Intersection-Types stillschweigend zu never kollabieren (siehe Abschnitt 10).
interface A {
name: string;
}
interface B {
name: number;
}
// Fehler — direkt auf der Deklaration:
// "Interface 'C' cannot simultaneously extend types 'A' and 'B'.
// Named property 'name' of types 'A' and 'B' are not identical."
// interface C extends A, B {}
// Was funktioniert: explizite Aufloesung im Sub-Interface.
interface C extends A, B {
// Eigene Deklaration überschreibt — muss aber zu BEIDEN passen.
// string & number ist never — also nicht erfuellbar.
// name: never;
}Der Vorteil dieser harten Fehlermeldung: du merkst es sofort an der Typ-Definition, nicht erst beim Versuch, einen Wert zu konstruieren. Intersection-Types behandeln denselben Fall im Verborgenen — die Property wird zu never, und der Fehler taucht erst weit entfernt bei der ersten Instanzierung auf. Wenn du also reine Objekt-Strukturen vereinst und unsicher bist, ob es Konflikte gibt: interface extends ist defensiver.
Property-Verengung beim extends
Ein Sub-Interface darf den Typ einer geerbten Property verengen — solange der neue Typ ein Subtyp des alten ist. Aus string darf "specific" werden, aus string | number darf string, aus Animal darf Dog. Umgekehrt — vom engeren zum breiteren Typ — ist nicht erlaubt, weil das den Vertrag des Eltern-Interfaces brechen wuerde.
interface Event {
// breiter Typ — jedes Event hat irgendeinen Namen.
type: string;
timestamp: number;
}
// ClickEvent verengt type auf ein konkretes Literal.
// Das ist erlaubt — "click" ist ein Subtyp von string.
interface ClickEvent extends Event {
type: "click";
x: number;
y: number;
}
const c: ClickEvent = {
type: "click",
timestamp: Date.now(),
x: 100,
y: 200,
};
// Gegenrichtung — verboten:
interface BadEvent extends ClickEvent {
// Fehler — "click" laesst sich nicht zu string erweitern.
// type: string;
type: "click" | "dblclick";
// Fehler — "click" | "dblclick" ist KEIN Subtyp von "click".
}Die Regel hinter dieser Asymmetrie nennt sich Liskov-Substitution: ein Wert vom Sub-Typ muss überall dort einsetzbar sein, wo der Super-Typ erwartet wird. Wenn ClickEvent type: string zu type: "dblclick" erweitern würde, könnte man ein ClickEvent nicht mehr als Event einsetzen — der Vertrag wäre gebrochen. TypeScript erzwingt diese Konsistenz auf Compiler-Ebene.
extends von type-Alias
Interfaces dürfen extends auf einen type-Alias anwenden — vorausgesetzt, der Alias löst zu einer Object-Form auf. Das ist die offizielle Brücke zwischen beiden Welten: du kannst einen Type-Alias als Basis nehmen und mit einem Interface fortsetzen.
// Type-Alias mit Object-Form.
type Identifiable = {
id: string;
};
// Interface erbt vom Alias — voellig legitim.
interface User extends Identifiable {
email: string;
}
const u: User = { id: "u-1", email: "a@x" };
// Auch Intersection-Typen sind erlaubt, solange sie Object-Form sind:
type Timestamped = { createdAt: Date };
type Versioned = { version: number };
interface Record extends Identifiable {
// Implementiert alle drei Properties.
createdAt: Date;
version: number;
}
// Fehler — Union-Types haben keine konsistente Object-Form:
// type StringOrObj = string | { id: string };
// interface X extends StringOrObj {} // nicht erlaubtPraktisch bedeutet das: du musst nicht aus religiöser Treue zu einer Welt — type oder interface — den Stil deiner Codebase brechen. Wenn ein Library-Type als type exportiert wird, kannst du ihn trotzdem mit interface ... extends weiterführen. Die einzige Bedingung: der Alias muss auf etwas zeigen, das wie ein Objekt aussieht — Primitives, Unions oder reine Funktions-Typen scheiden aus.
Class implements Interface
implements ist der Vertragsmechanismus zwischen einer Klasse und einem Interface. Eine Klasse, die implements I deklariert, verpflichtet sich, alle Member von I öffentlich bereitzustellen. Der Compiler prüft das bei jeder Methode, jedem Feld, jeder Property — fehlt etwas, gibt es einen Fehler.
interface Pingable {
ping(): void;
}
// Vertrag erfuellt — ping() ist vorhanden mit passender Signatur.
class Sonar implements Pingable {
ping() {
console.log("ping!");
}
}
// Vertrag NICHT erfuellt:
// class Ball implements Pingable {
// pong() { console.log("pong!"); }
// }
// Fehler — Property 'ping' is missing in type 'Ball' but required
// in type 'Pingable'.implements ist dabei rein deklarativ — es schafft keine Vererbungs-Beziehung. Die Klasse Sonar ist kein Subtyp von Pingable im Sinne einer Prototyp-Kette; sie ist lediglich strukturell kompatibel mit dem Interface. Im JS-Output verschwindet das implements-Keyword vollständig — es existiert nur zur Compile-Zeit.
implements verengt NICHT den Klassen-Typ
Ein subtiles, aber fundamental wichtiges Detail aus dem TypeScript-Handbook: class Foo implements Bar macht aus Foo nicht den Typ Bar. Die Klasse behält ihren eigenen Typ — Properties bleiben mit ihrem deklarierten Klassen-Typ erkannt, nicht mit dem oft breiteren Interface-Typ. Das hat zwei konkrete Konsequenzen.
interface Checkable {
// Interface deklariert: Parameter ist string.
check(name: string): boolean;
}
class NameChecker implements Checkable {
// Fehler — Parameter 's' implicitly has 'any' type.
// Das Interface übertraegt KEINE Typ-Annotationen auf die Methode.
check(s) {
return s.toLowerCase() === "ok";
}
}
// Korrekt — Typen müssen in der Klasse SELBST angegeben werden:
class NameCheckerOk implements Checkable {
check(s: string): boolean {
return s.toLowerCase() === "ok";
}
}Die zweite Konsequenz betrifft optionale Properties: wenn das Interface eine Property als optional deklariert, entsteht sie in der Klasse nicht automatisch. Implementiert die Klasse sie nicht explizit, ist sie schlicht nicht da — auch wenn das Interface sie kennt.
interface A {
x: number;
y?: number;
}
// Klasse implementiert nur x — y ist optional, also nicht Pflicht.
class C implements A {
x = 0;
// y wird NICHT automatisch hinzugefuegt.
}
const c = new C();
// Fehler — Property 'y' does not exist on type 'C'.
// c.y = 10;
// Auf dem Interface-Typ funktioniert es:
const a: A = c;
a.y = 10; // ok — A hat y? deklariert.Die Lektion: implements ist ein Vertrag, kein Casting. Die Klasse muss den Vertrag erfüllen, behält aber ihre eigene Identität. Wenn du den Interface-Typ brauchst, musst du eine Variable explizit damit annotieren — die Klasse alleine reicht nicht.
Mehrfach-implements
Wie Interfaces selbst dürfen auch Klassen mehrere Interfaces gleichzeitig implementieren. Die Klasse muss alle Verträge erfüllen — bei Konflikt zwischen den Interfaces wird die fehlende Konsistenz bei der Klassen-Implementierung gemeldet.
interface Serializable {
serialize(): string;
}
interface Comparable<T> {
compareTo(other: T): number;
}
// Klasse erfuellt beide Vertraege.
class Money implements Serializable, Comparable<Money> {
constructor(public amount: number, public currency: string) {}
serialize(): string {
return `${this.amount} ${this.currency}`;
}
compareTo(other: Money): number {
// Sehr vereinfacht — keine Wechselkurse beachtet.
return this.amount - other.amount;
}
}
const m1 = new Money(10, "EUR");
const m2 = new Money(20, "EUR");
console.log(m1.serialize()); // "10 EUR"
console.log(m1.compareTo(m2)); // -10Anders als bei class Foo extends Bar — wo nur eine Basisklasse erlaubt ist — kennt implements keine Begrenzung. Du kannst beliebig viele Interfaces auflisten, weil Interfaces nur Struktur und keine Implementierung mitbringen. Genau das macht Mehrfach-implements zu einer der saubersten Stellen in TypeScript, um aus einer Klasse einen vielseitigen Vertragspartner zu machen, ohne Mehrfach-Vererbung in JS nachbauen zu müssen.
abstract class vs. interface
Beide Konstrukte beschreiben einen Vertrag — aber mit unterschiedlichem Anspruch und unterschiedlichen Mitteln. Ein Interface beschreibt Struktur, ein abstract class beschreibt Struktur und kann teilweise Implementierungen mitliefern. Hier die wichtigsten Achsen im Vergleich:
| Aspekt | interface | abstract class |
|---|---|---|
| Implementierungen möglich | nein — nur Signaturen | ja — konkrete Methoden erlaubt |
| Instanzen direkt | nein | nein — nur via Subklasse |
| Mehrfach-Beziehung | class implements A, B | nur class extends Base (Single) |
| Runtime-Praesenz | keine — komplett geloescht | echte JS-Klasse im Output |
| Felder mit Default | nein | ja — readonly x = 5 etc. |
| Konstruktor | nein | ja |
| Zugriffsmodifier | implizit public | public / protected / private |
| Declaration Merging | ja | nein |
this-Bezuege | Signatur-Ebene möglich | volle Implementierung |
Faustregel: solange du nur Struktur beschreiben willst, nimm ein Interface — es ist leichter, schneller, und es lässt sich mehrfach implementieren. Sobald du gemeinsame Implementierung für mehrere Klassen teilen willst (Template-Methoden, geschützte Helfer, Standardverhalten), wechsle zu abstract class. Und wenn du beides brauchst — Vertrag plus Default-Verhalten — kombiniere: abstract class implements Interface. Die Klasse erfüllt das Interface, und ihre Subklassen erben sowohl Struktur als auch Default-Implementierungen.
extends vs. Intersection
Für reine Objekt-Typen sind interface B extends A und type C = A & B strukturell oft äquivalent. Es gibt aber zwei Achsen, an denen sie auseinanderlaufen — Konflikt-Verhalten und Reichweite.
interface A { id: string; name: string }
interface B { id: string; age: number }
// Variante 1 — interface extends. Konflikt? -> Compile-Fehler.
interface C1 extends A, B {} // ok, weil id in BEIDEN string ist.
// Variante 2 — Intersection. Konflikt? -> silent never.
type C2 = A & B; // ok, weil id konsistent ist.
// Streit-Fall:
interface Wide { tag: string }
interface Narrow { tag: number }
// interface extends — sofortiger Fehler:
// interface Conflict extends Wide, Narrow {}
// "Named property 'tag' of types 'Wide' and 'Narrow' are not identical."
// Intersection — KEIN Fehler bei der Definition:
type Conflict = Wide & Narrow;
declare const x: Conflict;
// x.tag hat den Typ never — Konstruktion eines Wertes kracht erst später.| Aspekt | interface extends | type mit & |
|---|---|---|
| Property-Konflikt | sofortiger Compile-Fehler | stillschweigend never |
| Primitives kombinieren | nicht möglich | möglich (string & "x") |
| Union als Bestandteil | nur indirekt | direkt ((A | B) & C) |
| Declaration Merging | ja | nein |
| Compiler-Performance | besser bei tiefen Hierarchien | tendenziell teurer |
Faustregel — und sie deckt sich mit der aus dem Intersection-Artikel: bei reinen Objekt-Strukturen ist interface extends defensiver und schneller. Sobald du Primitives, Unions oder Funktions-Typen in den Mix brauchst, führt kein Weg an type mit & vorbei — Interface kann das schlicht nicht ausdrücken.
Besonderheiten
extends bei Interfaces ist keine JS-Vererbung.
Es entsteht keine Prototypen-Kette, keine Laufzeit-Verbindung, kein Code-Output. interface B extends A ist eine reine Compile-Time-Beziehung; im transpilierten JavaScript existieren weder A noch B.
Property-Konflikt bei extends knallt sofort — bei Intersection schweigt es.
interface C extends A, B mit kollidierenden Property-Typen erzeugt einen direkten Compile-Fehler. type C = A & B reduziert die kollidierende Property still und leise auf never — der Fehler taucht erst bei der ersten Konstruktion auf. Bei reinen Objekten ist extends daher defensiver.
implements ist nur ein Vertrag, kein Casting.
class Foo implements Bar prüft, ob die Klasse alle Member von Bar bereitstellt — aber sie verengt den Klassen-Typ nicht. Property-Typen bleiben mit ihrem deklarierten Klassen-Typ erkannt. Wenn du den Interface-Typ brauchst, annotiere eine Variable explizit damit.
implements kann nur public Member pruefen.
Interfaces beschreiben öffentliche Struktur. private oder protected Felder einer Klasse sind für implements unsichtbar — das Interface kann sie weder fordern noch überpruefen. Vertrag und Implementierungs-Geheimnisse sind sauber getrennt.
Mehrfach-extends bei Interfaces ja — bei Klassen nein.
Ein Interface darf mehrere Eltern haben (interface C extends A, B), eine Klasse nur eine Basisklasse (class C extends Base). Klassen kompensieren das mit Mehrfach-implements plus Mixin-Patterns. Mehrfach-Vererbung auf Klassen-Ebene ist und bleibt nicht direkt unterstuetzt.
type-Alias kann ein Interface extenden — und umgekehrt.
interface B extends MyTypeAlias ist erlaubt, solange der Alias auf eine Object-Form auflöst. Das ist die offizielle Brücke zwischen type und interface: keine religiöse Trennung, nur eine strukturelle Bedingung.
TypeScript kennt keine echten abstract interfaces.
Alle Interface-Member sind "abstract" by default — sie deklarieren nur eine Signatur, niemals eine Implementierung. Wer Standard-Implementierungen teilen will, braucht eine abstract class. Interface ist immer pure Form.
implements ist ein TS-only-Keyword — im JS-Output verschwunden.
Genauso wie Interface-Deklarationen selbst loescht der Compiler auch das implements-Keyword komplett. Der ausgegebene JavaScript-Code zeigt nur die Klasse — kein Hinweis, dass sie irgendwann mal einen Vertrag erfuellen sollte. Reines Compile-Time-Tooling.
Object.create(prototype) + implements als funktionales Pattern.
Auch ohne class-Syntax kannst du einen Interface-Vertrag erfuellen: ein Plain-Object oder eine Factory, die Object.create mit einem Prototyp nutzt, kann als Wert dem Interface-Typ zugewiesen werden — strukturelles Typing erlaubt es. implements ist nur eine von vielen Wegen, den Vertrag zu deklarieren.
Weiterführende Ressourcen
Externe Quellen
- Extending Types – TypeScript Handbook
- implements Clauses – TypeScript Handbook
- Classes – TypeScript Handbook