Kaum eine TypeScript-Frage wird so religiös diskutiert wie „type oder interface?". In der Praxis sind die beiden Konstrukte zu 90 Prozent austauschbar — sie beschreiben beide Objektformen, lassen sich erweitern, mit Generics parametrisieren und in Klassen implementieren. Die echten Unterschiede liegen an den Rändern: interface beherrscht Declaration Merging und Module Augmentation, type kann Unions, Mapped Types und Conditional Types ausdrücken. Wer die Mechanik kennt, trifft die Entscheidung in Sekunden — und ohne ideologischen Beigeschmack. Dieser Artikel zeigt die Gemeinsamkeiten, die echten Differenzen und eine pragmatische Empfehlung, die das offizielle TypeScript-Team selbst formuliert.

Was beide gemeinsam haben

In den meisten Alltagsfällen sind type und interface deckungsgleich. Beide definieren Objektformen, beide unterstützen Generics, beide lassen sich von Klassen implementieren, beide erlauben Erweiterung — nur mit unterschiedlicher Syntax.

TypeScript Identische Objektform
interface I_User {
    id: number;
    name: string;
}

type T_User = {
    id: number;
    name: string;
};

// Beide sind strukturell austauschbar:
const a: I_User = { id: 1, name: "Anna" };
const b: T_User = a;   // + ok
const c: I_User = b;   // + ok

Auch Generics sehen nahezu gleich aus.

TypeScript Generics in beiden Welten
interface I_Box<T> {
    value: T;
}

type T_Box<T> = {
    value: T;
};

const x: I_Box<string> = { value: "hi" };
const y: T_Box<number> = { value: 42 };

Eine Klasse kann gegen beide Varianten implementieren — der Compiler unterscheidet hier nicht.

TypeScript implements funktioniert mit beidem
interface I_Greeter { greet(): void; }
type T_Greeter = { greet(): void };

class A implements I_Greeter {
    greet() { console.log("hi"); }
}

class B implements T_Greeter {
    greet() { console.log("hi"); }
}

Was nur interface kann

Der prominenteste Unterschied: Declaration Merging. Wird ein interface mit demselben Namen mehrfach deklariert, fasst TypeScript die Properties zu einer einzigen Definition zusammen. Mit type ist das ein harter Fehler.

TypeScript Declaration Merging
interface I_Config {
    host: string;
}

interface I_Config {
    port: number;
}

// beide Deklarationen werden gemerged:
const cfg: I_Config = { host: "localhost", port: 8080 };

Versucht man dasselbe mit type, blockiert der Compiler sofort.

TypeScript type kennt kein Merging
type T_Config = { host: string };
type T_Config = { port: number };   // − Fehler: Duplicate identifier

Aus Declaration Merging folgt Module Augmentation — die Fähigkeit, fremde Typen aus Drittpaketen oder dem globalen Scope nachträglich zu erweitern. Klassischer Anwendungsfall: zusätzliche Properties am Window-Objekt deklarieren.

TypeScript Window via Augmentation erweitern
declare global {
    interface Window {
        __APP_VERSION__: string;
        analytics: { track(event: string): void };
    }
}

window.__APP_VERSION__ = "1.4.2";   // + jetzt typisiert

Auch ein Modul lässt sich von außen erweitern — etwa um eine fremde Bibliothek mit zusätzlichen Methoden auszustatten.

TypeScript Modul-Augmentation
import "express";

declare module "express" {
    interface Request {
        requestId: string;
    }
}

Ein weiterer, oft übersehener Vorteil: bei sehr großen, tief verschachtelten Strukturen zeigt der TypeScript-Hover die Form eines interface meist mit Namen an, während ein type in eine inline-Struktur aufgelöst werden kann. In Fehlermeldungen erscheint die interface-Variante häufig benannt — bei der Suche nach Bugs in tiefen Generics ein Unterschied, der zählt.

Was nur type kann

Der type-Alias ist das vielseitigere Konstrukt. Alles, was kein reines Objekt-Shape ist, gehört in seine Domäne: Unions, Intersections, Tuples, Mapped Types, Conditional Types, Template Literal Types und typeof-Ableitungen.

TypeScript Union-Typen
type T_Status = "idle" | "loading" | "ready" | "error";
type T_Id = number | string;
TypeScript Tuple und Intersection
type T_Point = [number, number];
type T_Named = { name: string };
type T_Aged = { age: number };
type T_Person = T_Named & T_Aged;

Mapped Types und Conditional Types sind reines type-Territorium — interface kann diese Konstrukte syntaktisch gar nicht ausdrücken.

TypeScript Mapped Type
type T_Optional<T> = {
    [K in keyof T]?: T[K];
};

type T_PartialUser = T_Optional<{ id: number; name: string }>;
// { id?: number; name?: string }
TypeScript Conditional Type
type T_NonNullable<T> = T extends null | undefined ? never : T;
type T_Clean = T_NonNullable<string | null>;   // string

Template Literal Types und typeof-Ableitungen runden das Bild ab.

TypeScript Template Literal und typeof
type T_HttpVerb = "GET" | "POST" | "PUT" | "DELETE";
type T_Route = `/${string}`;
type T_Call = `${T_HttpVerb} ${T_Route}`;

const config = { host: "localhost", port: 8080 } as const;
type T_Config = typeof config;   // { readonly host: "localhost"; readonly port: 8080 }

Direkter Vergleich

Die folgende Tabelle fasst die Unterschiede in einer Übersicht zusammen.

Featuretypeinterface
Objektform definieren++
Generics++
ErweiterungIntersection &extends
Declaration Merging+
Module Augmentation+
Union-Typen+
Intersection-Typen+− (nur via extends)
Tuple-Typen+
Mapped Types+
Conditional Types+
Template Literal Types+
typeof-Ableitung+
Rekursion (direkt)nur via Indirection+
implements in Klassen++
Hover-Anzeige (groß)inline aufgelöstbenannt

Der einzige Bereich, in dem interface mehr kann als type, betrifft das Merging. Im Gegenzug ist type in fast jedem anderen Aspekt der mächtigere Begriff.

Strukturelle Kompatibilität

TypeScript prüft Typen strukturell — der Name spielt für die Kompatibilität keine Rolle. Solange die Form passt, sind type und interface in der Praxis vollständig austauschbar. Ein interface I_User und ein type T_User mit identischen Properties sind wechselseitig zuweisbar — auch über Modulgrenzen hinweg.

TypeScript Beide Welten mischen
interface I_User {
    id: number;
    name: string;
}

type T_User = {
    id: number;
    name: string;
};

function greet(u: I_User): void {
    console.log(u.name);
}

const x: T_User = { id: 1, name: "Anna" };
greet(x);   // + ok — strukturell identisch

Wer eine API gemischt aus beiden Konstrukten baut, wird in der Anwendung keinen Unterschied bemerken. Die Wahl ist also eher eine Frage von Ergonomie und Erweiterbarkeit als von Korrektheit.

Empfehlung der TypeScript-Community

Das offizielle TypeScript-Team formuliert die Empfehlung pragmatisch: „Use interface until you need features from type." Dahinter steht keine Ideologie, sondern eine schlichte Beobachtung — interface deckt den häufigsten Fall (Objekt-Shapes) ab und bietet mit Declaration Merging die Tür zur Erweiterbarkeit. Sobald Unions, Mapped Types oder Conditional Types ins Spiel kommen, ist type alternativlos.

Eine praxistaugliche Faustregel:

  • Public-API-Verträge (exportierte Typen, die Konsumenten kennen oder erweitern sollen): interface
  • Interne Kombinatorik (Helper-Typen, Unions, Mapped Types): type
  • Globale oder Drittparteien-Erweiterungen: zwingend interface

Diese Regel ist kein Dogma. In vielen Codebases — besonders kleineren — wird konsequent nur type verwendet, ohne dass irgendetwas verloren ginge. Konsistenz innerhalb eines Projekts ist wichtiger als die Wahl selbst.

Praxis-Beispiele

Eine Bibliothek exportiert ihre Public-API-Typen idealerweise als interface — so können Konsumenten sie via Module Augmentation erweitern.

TypeScript Library-API als interface
// lib/types.ts
export interface I_RequestContext {
    url: string;
    method: "GET" | "POST" | "PUT" | "DELETE";
    headers: Record<string, string>;
}

// app.ts — Konsument fügt Felder hinzu
declare module "lib/types" {
    interface I_RequestContext {
        userId?: string;
        traceId?: string;
    }
}

Interne Helper-Typen — abgeleitet, gemappt, kombiniert — bleiben sauber im type-Land.

TypeScript Helper-Typen als type
type T_ApiResult<T> =
    | { status: "ok"; data: T }
    | { status: "error"; message: string };

type T_Keys<T> = keyof T;
type T_ValueOf<T> = T[keyof T];

type T_PartialDeep<T> = {
    [K in keyof T]?: T[K] extends object ? T_PartialDeep<T[K]> : T[K];
};

Migration zwischen beiden

Der Wechsel von interface zu type und zurück ist meist trivial — solange die Form ein reines Objekt ist.

TypeScript interface zu type
// vorher
interface I_User {
    id: number;
    name: string;
}

// nachher
type T_User = {
    id: number;
    name: string;
};

Bei extends-Ketten wird aus jedem extends eine Intersection.

TypeScript extends-Kette zu Intersection
// vorher
interface I_Named { name: string; }
interface I_Aged { age: number; }
interface I_Person extends I_Named, I_Aged {
    id: number;
}

// nachher
type T_Named = { name: string };
type T_Aged = { age: number };
type T_Person = T_Named & T_Aged & { id: number };

Was beim Wechsel von interface zu type verloren geht: die Fähigkeit, die Definition später zu mergen oder per Module Augmentation zu erweitern. Wer einen exportierten Public-API-Typ migriert, sollte sicher sein, dass kein Konsument darauf angewiesen ist.

Umgekehrt — von type zu interface — ist die Migration nur möglich, wenn der type wirklich ein reines Objekt-Shape ist. Unions, Tuples und Mapped Types lassen sich nicht in interface übersetzen.

Performance

Im Compiler-Innenleben gibt es einen kleinen, oft zitierten Unterschied: interface-Vererbung via extends ist marginal schneller zu prüfen als die äquivalente Intersection bei type. Der Grund ist, dass extends-Beziehungen einmal aufgelöst und gecached werden, während Intersections bei jeder Prüfung neu berechnet werden können.

In Zahlen: der Unterschied bewegt sich in der Größenordnung von Mikrosekunden pro Prüfung. In einer durchschnittlichen Codebase ist das vollkommen irrelevant. Erst in sehr großen Projekten mit Tausenden tief verschachtelter Typen kann es messbar werden — und auch dort selten zur Engstelle. Das offizielle Performance-Wiki von TypeScript empfiehlt interface extends gegenüber type mit Intersection in heißen Pfaden, räumt aber ein, dass die Auswirkung in der Praxis fast immer vernachlässigbar bleibt.

TypeScript Mikro-Optimierung — selten relevant
// marginal schneller im Compiler:
interface I_A { a: number; }
interface I_B extends I_A { b: number; }

// marginal langsamer:
type T_A = { a: number };
type T_B = T_A & { b: number };

Wer auf Performance optimiert, sollte zuerst andere Hebel prüfen — skipLibCheck, Project References, incremental — bevor er einzelne type-Definitionen in interface überführt.

Besonderheiten

Declaration Merging als Pflicht-Mechanik

Beim Erweitern globaler Typen oder Modul-Typen aus Drittpaketen ist Declaration Merging die einzige Option. Window-Properties, Express-Request-Felder oder zusätzliche Methoden an Array.prototype — all das geht ausschließlich über interface. type hat hier strukturell keinen Hebel.

extends vs. Intersection — semantisch gleich

Ein interface B extends A und ein type B = A & { ... } erzeugen strukturell denselben Typ. Der Unterschied liegt nur in der Syntax und einer marginalen Performance-Notiz — funktional sind beide austauschbar.

Rekursive type-Aliase nur via Indirection

Direkte Rekursion wie type T = T | null lehnt TypeScript ab. Rekursion über eine Object- oder Array-Property funktioniert hingegen: type Tree = { value: number; children: Tree[] }. interface hat diese Einschränkung nicht und kann sich frei selbst referenzieren.

TypeScript-Style-Guide empfiehlt interface

Der offizielle Stil des TypeScript-Teams empfiehlt interface für „freistehende Objekt-Typen" — also exportierte, wiederverwendbare Verträge. Begründet wird das mit besserer Fehlermeldung, Erweiterbarkeit und einer minimal schnelleren Compiler-Prüfung.

Pick und Omit funktionieren auf beiden

Die Utility Types Pick<T, K>, Omit<T, K>, Partial<T> und Required<T> arbeiten gleichermaßen mit interface und type. Sie liefern in jedem Fall einen type-Alias zurück — was wieder zeigt, dass type das umfassendere Konstrukt ist.

interface kann type-Aliase extenden

Ein interface kann via extends auch einen type-Alias erweitern — solange dieser ein reines Objekt-Shape beschreibt. interface I_B extends T_A { b: number } ist gültig. Bei Unions oder Mapped Types als Quelle lehnt der Compiler ab.

implements akzeptiert beide

Eine Klasse darf sowohl implements I_Foo als auch implements T_Foo deklarieren, solange das Ziel ein Objekt-Shape ist. Mit einem Union-type als Implementierungsziel verweigert der Compiler — was logisch ist, weil eine Klasse keine Alternative sein kann.

IDE-Performance in großen Codebases

In sehr großen Codebases (5.000 plus Module) zeigen sich subtile Unterschiede: interface hält Hover-Tooltips und Quick-Info stabiler, weil der Compiler den Namen direkt anzeigen kann. type mit komplexer Intersection wird beim Hover oft komplett ausgewertet — was bei tiefen Generics zur spürbaren Latenz führen kann.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Type System

Zur Übersicht