Interface vs. Type Alias: Wann was verwenden
<T>
type
TypeScript · Type System · Interfaces · Type Aliases
Interface vs. Type Alias: Wann was verwenden
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.

13 Min. Lesezeit Interface · Type Alias · Type System Declaration Merging · Generics · Compiler

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.

11. FAQ: Interface vs. Type Alias

1Was ist der wichtigste technische Unterschied zwischen interface und type?
Declaration Merging: interface-Deklarationen mit demselben Namen verschmelzen automatisch, type-Aliase erlauben das nicht und werfen bei doppelter Deklaration einen Fehler.
2Kann ich eine Union mit interface ausdrücken?
Nein. Union Types wie 'a' | 'b' | 'c' lassen sich ausschließlich mit type abbilden, weil ein interface immer genau eine Objektform beschreibt.
3Wann sollte ich interface statt type verwenden?
Bei öffentlichen, exportierten Objektformen, die von anderen Modulen erweitert oder implementiert werden könnten, insbesondere bei API-Verträgen und Bibliotheks-Typen.
4Ist type wirklich langsamer als interface im Compiler?
Nur bei tief verschachtelten Intersections aus vielen Objekttypen. Interface-Typen werden stabiler gecacht. Für einfache Typen ist der Unterschied vernachlässigbar.
5Kann eine Klasse einen type-Alias implementieren?
Ja, solange der type-Alias eine reine Objektform beschreibt. Bei Unions oder primitiven Aliasen funktioniert implements nicht.
6Was sind Mapped Types und warum brauchen sie type?
Mapped Types erzeugen programmatisch neue Typen aus bestehenden Typen. Es gibt keine Interface-Syntax dafür, deshalb sind sie ausschließlich mit type möglich.
7Warum nutzen viele npm-Pakete interface statt type für öffentliche APIs?
Declaration Merging erlaubt Konsumenten, öffentliche Typen bei Bedarf zu erweitern, ohne den Originalcode der Bibliothek zu verändern.
8Gibt es eine ESLint-Regel, die die Konvention automatisch durchsetzt?
Ja, @typescript-eslint/consistent-type-definitions erzwingt projektweit ein einheitliches Muster für einfache Objektformen.
9Was passiert bei widersprüchlichen Eigenschaften in einer Intersection?
Oft entsteht der Typ never für die betroffene Eigenschaft, was Fehler erst spät im Code sichtbar macht statt sofort wie bei extends.
10Kann ich interface und type im selben Projekt mischen?
Technisch ja, empfohlen ist es aber nur nach klarer Konvention, um Konsistenz und Lesbarkeit über die gesamte Codebase zu erhalten.