Klare Regeln statt Bauchgefühl im Team
Interface und Type Alias wirken auf den ersten Blick austauschbar, doch Declaration Merging, Union Types und die Fähigkeiten des TypeScript-Compilers sorgen für echte Unterschiede. Dieser Artikel zeigt anhand konkreter Beispiele, wann jede Variante technisch notwendig ist, welche Team-Konvention sich in großen Codebasen bewährt und wo Performance und Wartbarkeit tatsächlich auseinanderlaufen.
Inhaltsverzeichnis
- 1. Die immer wiederkehrende Team-Diskussion: interface oder type?
- 2. Syntax-Unterschiede und gemeinsame Fähigkeiten
- 3. Declaration Merging: Warum interface offen bleibt
- 4. Type Alias für Unions, Intersections, Primitives und Tupel
- 5. extends vs. Intersection (&): Zwei Wege zur Komposition
- 6. Grenzfälle: Wo nur eine der beiden Varianten funktioniert
- 7. Compiler-Performance: Caching-Vorteile von Interfaces
- 8. Team-Konventionen für reale Codebasen
- 9. Interface vs. Type im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Die immer wiederkehrende Team-Diskussion: interface oder type?
In fast jedem TypeScript-Projekt gibt es früher oder später dieselbe Diskussion: Beschreibt man object shapes mit interface oder mit type? Beide Konstrukte überschneiden sich stark in dem, was sie leisten, und genau diese Überschneidung führt zu Reibung: Code-Reviews verzögern sich wegen Stilfragen, Style-Guides widersprechen sich zwischen Projekten, und neue Teammitglieder kopieren einfach das nächstbeste Beispiel aus der Codebase, ohne die eigentliche Begründung zu kennen.
Dabei ist die Antwort selten „reine Geschmackssache". Es gibt echte technische Unterschiede, die in bestimmten Situationen nur eine der beiden Varianten überhaupt zulassen. Dieser Artikel klärt genau das: wo beide Werkzeuge identisch funktionieren, wo nur eines der beiden tatsächlich funktioniert, und welche Konvention sich in der Praxis für ein Team bewährt, das über Monate und Jahre an derselben Codebase arbeitet. Wer die Unterschiede einmal verstanden hat, trifft die Entscheidung nicht mehr aus Gewohnheit, sondern aus einem konkreten technischen Grund.
2. Syntax-Unterschiede und gemeinsame Fähigkeiten
Rein syntaktisch unterscheiden sich interface und type bei einfachen Objektformen kaum: Beide beschreiben Felder mit ihren Typen, beide unterstützen optionale Eigenschaften mit ?, beide kennen readonly-Modifikatoren, und beide lassen sich mit Generics parametrisieren. Wer nur objektartige Strukturen modelliert, merkt in der Praxis lange keinen Unterschied. Der eigentliche Unterschied liegt in der Deklaration selbst: interface ist eine eigene Sprachkonstrukt-Kategorie mit eigener Merge-Semantik, während type schlicht einen Namen an einen beliebigen Typ bindet, egal ob Objekt, Union, Tupel oder primitiver Typ.
Auch Funktionssignaturen lassen sich mit beiden Werkzeugen ausdrücken, entweder als Call-Signature innerhalb eines interface-Blocks oder als Funktionstyp hinter einem type-Alias. Index-Signaturen funktionieren ebenfalls in beiden Varianten. Der praktische Unterschied zeigt sich erst, sobald man über reine Objektformen hinausgeht: Sobald Union Types, Tupel, Mapped Types oder Template Literal Types im Spiel sind, bleibt nur type übrig, wie die folgenden Abschnitte zeigen.
// Both interface and type can describe the same object shape
interface UserInterface {
id: number;
name: string;
email: string;
}
type UserType = {
id: number;
name: string;
email: string;
};
// Both support optional and readonly properties
interface ProductInterface {
readonly sku: string;
price: number;
discount?: number;
}
// Both can describe function signatures
interface Logger {
(message: string): void;
}
type LoggerType = (message: string) => void;
// Both support generics
interface Box<T> {
value: T;
}
type BoxType<T> = {
value: T;
};
3. Declaration Merging: Warum interface offen bleibt
Der wohl wichtigste strukturelle Unterschied ist Declaration Merging: Deklariert man ein interface mit demselben Namen mehrfach im selben Scope, verschmilzt TypeScript automatisch alle Mitglieder zu einer einzigen effektiven Deklaration. Ein type-Alias verhält sich hier komplett anders: Ein zweiter type mit identischem Namen im selben Scope ist ein Compile-Fehler wegen doppelter Bezeichner. Interfaces sind also von Natur aus „offen", Type Aliases sind „geschlossen".
Praktisch relevant wird das bei der Erweiterung von Drittanbieter-Typen. Wer beispielsweise Express-Request-Objekte um ein eigenes Feld wie tenantId ergänzen will, oder ein globales Window-Objekt um ein von einem Analytics-Skript injiziertes Feld wie dataLayer, kommt um Declaration Merging nicht herum. Auch viele npm-Pakete nutzen dieses Pattern selbst, um ihre öffentlichen Typen erweiterbar zu halten. Genau deshalb definieren gut gepflegte Bibliotheken ihre öffentlichen APIs bevorzugt als interface, nicht als type - sie ermöglichen damit Konsumenten, die Typen bei Bedarf zu ergänzen, ohne den Originalcode zu verändern.
// Declaration merging: augmenting a third-party interface
// This works because interfaces are open, type aliases are not
declare namespace Express {
interface Request {
// Add a custom property to Express Request without touching its source
tenantId?: string;
}
}
// Merging is also used to extend global objects
interface Window {
// Third-party analytics script attaches itself to window at runtime
dataLayer: Record<string, unknown>[];
}
// Two separate interface declarations with the same name merge automatically
interface Config {
apiUrl: string;
}
interface Config {
timeout: number;
}
// Config now has both apiUrl and timeout
const config: Config = {
apiUrl: 'https://api.mironsoft.de',
timeout: 5000,
};
// This would be a compile error: type aliases cannot be re-opened
// type Config = { apiUrl: string };
// type Config = { timeout: number }; // Error: Duplicate identifier 'Config'
4. Type Alias für Unions, Intersections, Primitives und Tupel
Sobald mehr als eine reine Objektform beschrieben werden soll, ist type alternativlos. Union Types wie 'pending' | 'paid' | 'shipped' lassen sich mit interface überhaupt nicht ausdrücken, weil ein Interface immer genau eine Objektform beschreibt, keine Alternative von Werten. Dasselbe gilt für primitive Aliase wie type ProductId = string, für Tupel wie type Coordinates = [number, number] und für reine Funktionstypen, die sich zwar auch über eine Call-Signature in einem Interface abbilden lassen, bei denen type aber deutlich kompakter ist.
Besonders wertvoll sind Discriminated Unions: eine Union aus mehreren Objektvarianten mit einem gemeinsamen literalen Feld, das TypeScript zur Typverengung in switch- oder if-Blöcken nutzt. Dieses Pattern ist in Frontend-Projekten extrem häufig, etwa bei der Modellierung unterschiedlicher Zahlungsarten oder API-Antwortformen mit Erfolgs- und Fehlerfall. Ohne type-Aliase für Unions wäre dieses Pattern in der heutigen Form schlicht nicht umsetzbar, weil Interfaces strukturell nicht für „entweder-oder"-Beziehungen gedacht sind.
// Only a type alias can name a union of primitives or literals
type OrderStatus = 'pending' | 'paid' | 'shipped' | 'cancelled';
// Interfaces cannot express this directly:
// interface OrderStatus = 'pending' | 'paid'; // not valid syntax
// Type aliases can name primitives, tuples and function types directly
type ProductId = string;
type Coordinates = [latitude: number, longitude: number];
type Comparator<T> = (a: T, b: T) => number;
// Discriminated unions are a common real-world case
type PaymentMethod =
| { type: 'creditCard'; cardNumber: string }
| { type: 'paypal'; email: string }
| { type: 'invoice'; iban: string };
function processPayment(method: PaymentMethod): void {
switch (method.type) {
case 'creditCard':
// TypeScript narrows to the creditCard variant here
console.log(method.cardNumber);
break;
case 'paypal':
console.log(method.email);
break;
case 'invoice':
console.log(method.iban);
break;
}
}
5. extends vs. Intersection (&): Zwei Wege zur Komposition
Beide Werkzeuge bieten Komposition, aber mit unterschiedlicher Mechanik. interface nutzt extends und prüft dabei die Kompatibilität der erweiterten Typen bereits bei der Deklaration: Wenn eine Eigenschaft im Subtyp einen inkompatiblen Typ zur Basis hat, meldet TypeScript den Fehler sofort an der extends-Klausel. Type Aliases nutzen stattdessen den Intersection-Operator &, der zwei Typen strukturell zusammenführt. Bei widersprüchlichen Eigenschaften ergibt eine Intersection nicht automatisch einen Fehler, sondern im schlimmsten Fall den Typ never für die betroffene Eigenschaft, was Fehler oft erst später im Code sichtbar macht.
Ein Interface kann außerdem mehrere andere Interfaces gleichzeitig erweitern, was in tief geschichteten Domänenmodellen die Lesbarkeit deutlich verbessert. Für Klassen ist implements die Brücke zwischen beiden Welten: Eine Klasse kann sowohl ein interface als auch einen objektartigen type-Alias implementieren, solange dieser eine reine Objektform beschreibt. Bei Unions oder primitiven Type Aliases ist implements dagegen nicht möglich, weil eine Klasseninstanz strukturell immer eine feste Objektform hat.
// Interface composition via extends
interface BaseEntity {
id: number;
createdAt: Date;
}
interface Product extends BaseEntity {
sku: string;
price: number;
}
// Type alias composition via intersection
type BaseEntityType = {
id: number;
createdAt: Date;
};
type ProductType = BaseEntityType & {
sku: string;
price: number;
};
// Interfaces can extend multiple interfaces at once
interface Timestamped {
updatedAt: Date;
}
interface AuditableProduct extends Product, Timestamped {
editedBy: string;
}
// A class can only "implements" object-shaped contracts,
// both interface and object type aliases work here
class DatabaseProduct implements Product {
constructor(
public id: number,
public createdAt: Date,
public sku: string,
public price: number,
) {}
}
6. Grenzfälle: Wo nur eine der beiden Varianten funktioniert
Manche TypeScript-Features sind schlicht nur mit type umsetzbar. Mapped Types wie { [K in keyof T]: ... } erzeugen aus einem bestehenden Typ programmatisch einen neuen Typ und haben keine Interface-Entsprechung. Conditional Types mit extends und infer funktionieren ebenfalls ausschließlich über type, genauso wie Template Literal Types, die aus String-Literalen zur Compile-Zeit neue String-Typen zusammensetzen. Wer generische Utility-Typen wie eigene Varianten von Partial oder Pick selbst schreibt, landet also fast zwangsläufig bei type.
Umgekehrt gibt es kaum ein Feature, das ausschließlich interface vorbehalten ist, außer Declaration Merging selbst. Ein wichtiger Praxishinweis: Die eingebauten Utility-Typen wie Pick<T, K> oder Partial<T> funktionieren technisch auf beiden Eingabetypen und liefern immer ein type als Ergebnis zurück, unabhängig davon, ob die Quelle ein interface oder ein type war. Diese Asymmetrie, kombiniert mit den Union- und Mapped-Type-Einschränkungen, erklärt, warum viele TypeScript-Codebasen mit wachsender Komplexität tendenziell mehr type-Aliase als Interfaces ansammeln.
// Mapped types require "type", there is no interface equivalent
type ReadonlyProduct<T> = {
readonly [K in keyof T]: T[K];
};
// Conditional types also require "type"
type ExtractId<T> = T extends { id: infer U } ? U : never;
type ProductIdOnly = ExtractId<{ id: number; name: string }>; // number
// Template literal types are only possible with "type"
type EventName = `on${Capitalize<'save' | 'delete' | 'publish'>}`;
// resolves to: 'onSave' | 'onDelete' | 'onPublish'
// Utility types like Partial, Pick and Omit return type aliases,
// but they work perfectly fine on interfaces as input
interface DraftProduct {
sku: string;
price: number;
description: string;
}
type ProductPreview = Pick<DraftProduct, 'sku' | 'price'>;
type OptionalProduct = Partial<DraftProduct>;
7. Compiler-Performance: Caching-Vorteile von Interfaces
Das TypeScript-Team selbst weist darauf hin, dass interface in bestimmten Szenarien schneller vom Compiler verarbeitet werden kann als eine strukturell äquivalente Intersection aus type-Aliasen. Der Grund liegt in der internen Repräsentation: Ein Interface wird als benannter, flacher Typ mit einer stabilen Identität gecacht, während eine Intersection bei jeder Verwendung potenziell neu aufgelöst und strukturell abgeglichen werden muss, besonders wenn mehrere Intersections verschachtelt sind. Bei kleinen Codebasen ist dieser Unterschied nicht messbar, bei großen Monorepos mit tausenden Typprüfungen pro Build kann er sich jedoch spürbar auf die tsc-Laufzeit auswirken.
Wichtig ist die Einordnung: Dieser Performance-Vorteil betrifft primär tief verschachtelte Intersections vieler Objekttypen, nicht jeden beliebigen type-Alias. Ein einfacher type für eine flache Objektform oder eine Union verursacht keinen relevanten Mehraufwand. Als Faustregel gilt: Wer sehr große, oft wiederverwendete Objektinterfaces baut, die an vielen Stellen erweitert werden, profitiert von interface und extends statt einer langen Kette aus Intersections. Für alles andere ist der Performance-Unterschied in der Praxis vernachlässigbar gegenüber Lesbarkeit und Team-Konvention.
8. Team-Konventionen für reale Codebasen
Die Frage „interface oder type" lohnt sich in jedem Projekt einmal grundsätzlich zu klären, statt sie bei jedem Pull Request neu zu diskutieren. Eine bewährte, in vielen TypeScript-Style-Guides verbreitete Konvention lautet: interface für öffentliche, exportierte Objektformen, die von anderen Modulen implementiert oder erweitert werden könnten, insbesondere bei API-Verträgen und Domänenmodellen. type für alles, was Interfaces strukturell nicht können, also Unions, Intersections, Tupel, primitive Aliase und abgeleitete Utility-Typen.
Ein ESLint-Regelwerk wie @typescript-eslint/consistent-type-definitions setzt diese Konvention automatisiert durch und verhindert, dass verschiedene Teammitglieder für dieselbe Art von Typ unterschiedliche Werkzeuge wählen. Wichtiger als die konkrete Regel ist die Konsistenz selbst: Ein Team, das durchgängig demselben Muster folgt, reduziert kognitive Last bei Code-Reviews und macht Diffs vorhersehbarer. In Projekten mit TypeScript-Build-Skripten oder Headless-Frontends empfiehlt sich zusätzlich, Interfaces für alle GraphQL- oder REST-Antwortformen zu verwenden, weil diese häufig um zusätzliche Felder erweitert werden, sobald neue API-Versionen erscheinen.
9. Interface vs. Type im direkten Vergleich
Die folgende Tabelle fasst zusammen, welche Fähigkeiten interface und type jeweils tatsächlich unterstützen. Sie eignet sich als schnelle Referenz für die nächste Team-Diskussion oder den nächsten Code-Review-Kommentar.
| Fähigkeit | interface | type |
|---|---|---|
| Declaration Merging | Unterstützt | Nicht unterstützt |
| Union Types | Nicht unterstützt | Unterstützt |
| Primitive Aliase | Nicht unterstützt | Unterstützt |
| extends mehrere Typen | Ja (extends A, B) | Ja (A & B & C) |
| implements in Klassen | Ja | Ja, nur bei Objektform |
| Mapped / Conditional Types | Nicht unterstützt | Unterstützt |
In der Praxis zeigt die Tabelle vor allem eins: Die beiden Werkzeuge überschneiden sich stark, aber nicht vollständig, und die Lücken sind fast nie symmetrisch. Wer eine klare Konvention etabliert und die hier gezeigten Grenzfälle kennt, muss die Entscheidung nicht mehr pro Zeile treffen, sondern nur noch einmal pro Projekt.
Mironsoft
TypeScript-Code-Reviews und Architektur-Beratung für Magento- und Hyvä-Frontends
Klare TypeScript-Konventionen für euer Team?
Wir reviewen eure bestehende Codebase, identifizieren inkonsistente Interface/Type-Nutzung und etablieren gemeinsam mit eurem Team eine Konvention, die sich in der Praxis durchsetzt - von ESLint-Regeln bis zur Architektur typsicherer API-Clients.
TypeScript Code-Review
Bestehende Typdefinitionen analysieren, Inkonsistenzen und Grenzfälle aufdecken
Codebase-Konventionen etablieren
ESLint-Regeln, Style-Guides und Team-Onboarding für konsistenten Typ-Code
Typsichere Frontend-Architektur
API-Clients, GraphQL-Typen und Headless-Integrationen sauber modellieren
10. Zusammenfassung
Interface und Type Alias sind kein Entweder-Oder, sondern zwei Werkzeuge mit unterschiedlichen Stärken. interface punktet bei Declaration Merging, bei der Erweiterung von Drittanbieter-Typen und potenziell bei der Compiler-Performance großer, verschachtelter Objektmodelle. type ist die einzige Option für Unions, Intersections, Tupel, primitive Aliase, Mapped Types und Conditional Types. Wo beide funktionieren, etwa bei einfachen Objektformen, entscheidet am Ende die Team-Konvention, nicht die Sprache selbst.
Wer die hier beschriebenen Grenzfälle kennt, wenn also nur eine der beiden Varianten technisch möglich ist, muss nicht mehr raten. Für alle anderen Fälle lohnt sich eine dokumentierte, per Linter durchgesetzte Konvention, die neue Teammitglieder ohne Diskussion übernehmen können. Das reduziert Reibung in Code-Reviews und macht die Codebase über Jahre hinweg konsistenter lesbar.
Interface vs. Type Alias - Das Wichtigste auf einen Blick
Declaration Merging
interface erlaubt mehrfaches Deklarieren, type nicht - Grundlage für Third-Party-Erweiterungen.
Unions & Primitives
Nur type kann Unions, Tupel und primitive Aliase ausdrücken.
Performance
interface + extends kann bei großen, verschachtelten Modellen schneller kompilieren als lange Intersection-Ketten.
Team-Konvention
ESLint-Regel wie consistent-type-definitions durchsetzen, Konsistenz vor persönlicher Präferenz.