Intersection Types sind das logische Gegenstück zu Unions: wo ein Union-Typ „entweder A oder B" ausdrückt, fordert eine Intersection „A und B gleichzeitig". Das Werkzeug dafür ist das Ampersand & — eine einzige Zeichen, die mehrere Typen zu einem zusammenfügt, der alle Eigenschaften der Bestandteile erbt. Dieser Artikel zeigt die Mengen-theoretische Bedeutung, das Mixin-Pattern zur sauberen Objekt-Komposition und die Eigenheiten, wenn Intersection auf Primitives oder kollidierende Properties trifft. Du lernst, warum string & number als never herauskommt, wieso Property-Konflikte oft stillschweigend zu never führen, und an welcher Stelle Intersections gegenüber interface extends die Nase vorn haben — und umgekehrt.

Was Intersection Types sind

Eine Intersection kombiniert mehrere Typen zu einem neuen Typ, dessen Werte alle Bestandteile gleichzeitig erfüllen müssen. Formal: wenn A die Menge der Werte ist, die Typ A erfüllen, und B die Menge der Werte für Typ B, dann ist A & B die Schnittmenge beider Mengen — alle Werte, die in beiden Mengen liegen.

Die Syntax ist denkbar knapp: ein einzelnes & zwischen zwei Typen. Mehr braucht es nicht.

ts intersection-basics.ts
interface Named {
    name: string;
}

interface Aged {
    age: number;
}

// Schnittmenge: ein Wert muss SOWOHL name ALS AUCH age haben.
type Person = Named & Aged;

// Gültig — beide Properties vorhanden, beide passen typ-technisch.
const anna: Person = { name: "Anna", age: 34 };

// Fehler — age fehlt, Intersection ist nicht erfüllt.
// const bob: Person = { name: "Bob" };

// Fehler — name hat falschen Typ.
// const carl: Person = { name: 42, age: 30 };

Der entscheidende Kontrast zu Unions liegt in der Logik: A | B ist ein ODER — ein Wert muss nur eine der beiden Bedingungen erfüllen. A & B ist ein UND — ein Wert muss beide gleichzeitig erfüllen. Klingt einfach, hat aber tiefe Konsequenzen für die Werte-Menge, wie der nächste Abschnitt zeigt.

Intersection als Werte-Menge

Es lohnt sich, das Bild zweier überlappender Kreise im Kopf zu haben. Named ist alles, was einen name: string hat — viele Werte, weltweit. Aged ist alles mit age: number. Die Intersection Named & Aged ist der Überlappungsbereich: Objekte, die beide Properties zugleich tragen.

ts set-view.ts
interface Named { name: string }
interface Aged  { age: number }

// Genau in der Überlappung: erfüllt Named UND Aged.
const p1: Named & Aged = { name: "Anna", age: 34 };

// Nur in Named — kein age:
const p2: Named = { name: "Bob" };
// const test: Named & Aged = p2; // Fehler — age fehlt.

// Zusätzliche Properties stören nicht, solange beide Pflicht-Teile da sind:
const p3: Named & Aged = { name: "Carl", age: 12, role: "kid" };

Wichtig zu verstehen: Intersection fordert mehr, nicht weniger. Die Werte-Menge wird kleiner, je mehr Typen du mit & verbindest — denn jeder zusätzliche Bestandteil ist eine weitere Bedingung. Genau umgekehrt wie bei Unions, wo jeder zusätzliche Bestandteil die Menge vergrößert.

Aus dieser Mengen-Sicht ergibt sich auch, warum eine Intersection sehr schnell leer werden kann: sobald zwei Bestandteile widersprüchliche Anforderungen stellen, gibt es keinen Wert mehr, der beide erfüllt — und der Typ kollabiert zu never. Dazu gleich mehr.

Mixin-Pattern für Objekte

Der mit Abstand häufigste Praxis-Einsatz von Intersection ist das Mixin: bestehende Objekt-Typen werden zu einem neuen, reicheren Typ kombiniert, ohne dass eine Klassenhierarchie aufgebaut werden müsste. Klassische Beispiele: Audit-Felder, Timestamps, Soft-Delete-Markierungen oder Owner-Referenzen — Dinge, die viele Entitäten teilen, aber nicht jede.

ts mixins.ts
// Basis-Entität: nur fachliche Daten.
interface User {
    id: string;
    email: string;
}

// Wer hat zuletzt geändert? Wer hat erstellt? Quer-Concern.
interface Auditable {
    createdBy: string;
    updatedBy: string;
}

// Wann erstellt/geändert? Ebenfalls Quer-Concern.
interface Timestamped {
    createdAt: Date;
    updatedAt: Date;
}

// Komposition statt Vererbung — drei orthogonale Aspekte in einem Typ.
type AuditedUser = User & Auditable & Timestamped;

const record: AuditedUser = {
    id: "u-1",
    email: "anna@example.com",
    createdBy: "system",
    updatedBy: "system",
    createdAt: new Date("2026-01-01"),
    updatedAt: new Date(),
};

Warum Mixin per Intersection statt Vererbung? Drei Gründe:

  • OrthogonalitätAuditable und Timestamped haben nichts miteinander zu tun. Eine Vererbungskette User extends Timestamped extends Auditable würde eine künstliche Hierarchie suggerieren.
  • Wiederverwendbarkeit — dieselben Mixins lassen sich an Order, Invoice, Comment etc. anflanschen, ohne dass irgendeine Basisklasse alle Fälle abdecken muss.
  • LesbarkeitUser & Auditable & Timestamped liest sich wie eine Zutaten-Liste; der Leser sieht auf einen Blick, was zusammenkommt.

Das Runtime-Pendant zu so einer Intersection ist übrigens Object.assign(a, b, c) oder das Spread-Pattern { ...a, ...b, ...c } — beide produzieren ein Objekt, das die Felder aller Quellen vereint. Intersection beschreibt auf Typebene genau das, was diese JS-Operationen zur Laufzeit tun.

Intersection mit Primitives

Intersections sind nicht auf Objekt-Typen beschränkt. Du darfst auch Primitives mit & verbinden — nur passiert dabei oft etwas, das auf den ersten Blick verwirrend ist:

ts primitives.ts
// Welcher Wert ist GLEICHZEITIG string und number?
// Keiner. Die Schnittmenge ist leer.
type Impossible = string & number; // -> never

// Genauso:
type AlsoNever = boolean & string;  // -> never
type AndAgain  = "a" & "b";         // -> never (verschiedene Literal-Typen)

// Aber das hier geht — gleiche Literal-Typen kollidieren nicht:
type SameLiteral = "a" & "a";       // -> "a"

// Und das hier ist nützlich — Literal verfeinert Primitive:
type Hello = string & "hello";      // -> "hello"

Die Logik ist sauber: ein Wert muss string UND number sein — den gibt es nicht, also ist die Schnittmenge leer, und der leere Typ in TypeScript heißt never. Das ist kein Fehler, sondern eine korrekte Typ-Schlussfolgerung.

Praktisch nützlich wird Intersection mit Primitives in Kombination mit Tag-Objekten — Stichwort Branded Types, siehe Abschnitt 8. Dort wird ein Primitive (string, number) mit einem zusätzlichen Marker-Objekt geschnitten, um Nominal-Typing nachzubilden.

Property-Konflikte

Heikler — weil oft stillschweigend problematisch — sind Konflikte auf Property-Ebene. Wenn zwei Bestandteile einer Intersection denselben Property-Key mit unterschiedlichen Typen tragen, errechnet der Compiler die Schnittmenge der Property-Typen. Ist die Schnittmenge leer, wird die Property zu never — und die Intersection wird in der Praxis unbrauchbar:

ts conflicts.ts
interface A { id: string }
interface B { id: number }

// id muss SOWOHL string ALS AUCH number sein -> never.
type AB = A & B;

declare const x: AB;
// x.id hat den Typ never — kein Wert kann diese Bedingung erfüllen.
// const s: string = x.id; // technisch zuweisbar, weil never zu allem passt,
//                         // aber das Konstruieren eines x ist unmöglich.

// Schlimmer: TypeScript wirft KEINEN Fehler auf der Typ-Definition selbst.
// Erst beim Versuch, einen Wert vom Typ AB zu erzeugen, kracht es.
// const broken: AB = { id: "1" };  // Fehler — id ist not assignable to never
// const broken: AB = { id: 1 };    // Fehler — id ist not assignable to never

Das ist eine der Anti-Pattern-Fallen mit Intersection: man baut eine Schnittmenge zusammen, die nie erfüllbar ist, ohne dass die Definition selbst gemeldet wird. Erst die Konstruktion einer Instanz scheitert — manchmal weit entfernt von der Stelle, an der der eigentliche Typ entstand.

Gegenbeispiel: wenn die Property-Typen kompatibel sind, ergibt der Schnitt einen sinnvollen, oft engeren Typ:

ts compatible.ts
interface Wide  { kind: string }
interface Narrow { kind: "admin" | "user" }

// kind muss string UND ("admin" | "user") sein -> "admin" | "user".
type Both = Wide & Narrow;

const ok: Both = { kind: "admin" };
// const bad: Both = { kind: "guest" }; // Fehler — guest ist kein "admin" | "user".

Hier engt die Intersection den breiteren string auf das Literal-Union ein. Das ist legitim und manchmal genau gewollt — eine Intersection als „type refinement".

Intersection vs. interface extends

Für reine Objekt-Typen gibt es zwei Wege, Eigenschaften zu bündeln: interface B extends A und type B = A & {...}. In den meisten Fällen sind sie austauschbar — aber nicht in allen.

ts extends-vs-intersection.ts
interface Base {
    id: string;
}

// Variante 1 — interface extends.
interface UserA extends Base {
    email: string;
}

// Variante 2 — Intersection auf Type-Alias-Ebene.
type UserB = Base & {
    email: string;
};

// Beide ergeben strukturell denselben Typ.
const a: UserA = { id: "1", email: "a@x" };
const b: UserB = a; // ok — strukturell identisch.

Unterschiede gibt es bei Konflikten und bei Reichweite:

Aspektinterface extendstype mit &
Property-Konfliktsofortiger Compile-Fehlerstillschweigend never
Primitives kombinierennicht möglichmöglich (string & "literal")
Union als Bestandteilnur indirektdirekt ((A | B) & C)
Declaration Mergingja — mehrere interface mit gleichem Namen verschmelzennein
Compiler-Performancetendenziell besserbei tiefen Intersections teurer

Faustregel: solange du reine Objekt-Strukturen kombinierst, bevorzuge interface extends — die Fehlermeldungen sind expliziter und der Compiler ist schneller. Sobald Primitives, Unions oder Funktions-Typen ins Spiel kommen, brauchst du type mit &, weil interface das nicht abbilden kann.

Intersection mit Funktions-Typen

Ein weniger bekanntes, aber sehr mächtiges Idiom: Intersection von Call-Signatures zur Simulation von Funktions-Overloads.

ts function-overloads.ts
// Zwei separate Funktions-Typen — jeder mit eigener Signatur.
type ToStringFromNumber = (input: number) => string;
type ToStringFromBool   = (input: boolean) => string;

// Intersection -> ein Funktions-Typ mit beiden Aufruf-Varianten.
type Stringify = ToStringFromNumber & ToStringFromBool;

// Konkrete Implementierung muss BEIDE Signaturen erfüllen können:
const stringify: Stringify = ((x: number | boolean) => String(x)) as Stringify;

const s1 = stringify(42);    // ok — number-Overload.
const s2 = stringify(true);  // ok — boolean-Overload.
// const s3 = stringify("x"); // Fehler — string ist in keiner Signatur.

Was hier passiert: ein Wert vom Typ A & B muss als A und als B verwendbar sein. Wenn A und B Funktions-Signaturen sind, heißt das: die Funktion muss mit beiden Argument-Typen aufrufbar sein. Praktisch ein Overload, nur ohne das function-Overload-Keyword-Gerüst.

Caveat: bei der Implementierung musst du den Cast oft selbst setzen, weil TypeScript nicht automatisch aus einem (x: number | boolean) => string einen Intersection-Typ ableitet. Die Konstruktions-Seite ist dadurch etwas umständlich; die Konsum-Seite liest sich aber sehr sauber.

Praxis: Branded Types

Ein kurzer Vorgriff auf ein größeres Thema: Branded Types nutzen Intersection, um in TypeScripts strukturellem Typsystem nominale Typen nachzubauen — also Typen, die nicht alleine durch ihre Form, sondern durch eine künstliche Marke unterscheidbar sind.

ts branded.ts
// Trick: ein Primitive wird mit einem unsichtbaren Marker-Objekt geschnitten.
// Der Marker existiert nur auf Typebene — zur Laufzeit ist es ein normaler string.
type UserId  = string & { readonly __brand: "UserId" };
type OrderId = string & { readonly __brand: "OrderId" };

function loadUser(id: UserId)  { /* ... */ }
function loadOrder(id: OrderId) { /* ... */ }

const raw = "abc-123";
// loadUser(raw); // Fehler — string fehlt das Brand.

// Kontrollierter Eintritt über expliziten Cast (oder eine Factory):
const userId = raw as UserId;
loadUser(userId); // ok

// Verwechslung wird auf Typebene verhindert:
// loadOrder(userId); // Fehler — UserId ist nicht OrderId.

Der Schlüssel ist die Intersection string & { __brand: ... }: der Wert bleibt ein echter string und ist mit allen string-Operationen kompatibel, trägt aber gleichzeitig eine eindeutige Markierung, die jede Verwechslung mit anderen Brand-Typen ausschließt. Mehr Tiefe zu diesem Pattern — inklusive Factory-Funktionen und unique symbol-Varianten — findest du im Artikel Aliase und Namenstypen.

Reihenfolge irrelevant

Intersection ist kommutativ und assoziativ: A & B ist identisch mit B & A, und (A & B) & C ist identisch mit A & (B & C). Mathematisch ist das die übliche Eigenschaft des Mengen-Durchschnitts.

ts commutative.ts
interface Named { name: string }
interface Aged  { age: number }

type T1 = Named & Aged;
type T2 = Aged & Named;

// Strukturell identisch — gegenseitig zuweisbar.
const a: T1 = { name: "Anna", age: 34 };
const b: T2 = a;
const c: T1 = b;

Praktisch relevant ist trotzdem ein Detail: die Reihenfolge der Bestandteile beeinflusst die Anzeige im Editor-Hover und in Fehlermeldungen. User & Auditable & Timestamped zeigt im IntelliSense die Properties oft in der Reihenfolge, in der die Typen aufgelistet sind. Für die Typ-Identitaet macht das nichts aus, für die Lesbarkeit der Tooltips schon — gewöhne dir an, die fachliche Basis nach links zu setzen und Quer-Concerns nach rechts.

Interessantes

Intersection ist Schnitt-Menge, nicht Vereinigung.

A & B beschreibt Werte, die sowohl A als auch B erfüllen. Jeder zusätzliche Bestandteil engt die Werte-Menge weiter ein — das genaue Gegenteil von Unions, wo jeder Bestandteil die Menge vergrößert.

T & never ist never — never ist absorbierend bei Intersection.

Sobald irgendwo in einer Intersection ein never steht, kollabiert das gesamte Ergebnis zu never. Das ist konsistent mit der Mengen-Theorie: die Schnittmenge mit der leeren Menge ist die leere Menge.

T & unknown ist T — unknown ist neutral bei Intersection.

unknown verhält sich bei Intersection wie das neutrale Element: es liefert keine zusätzliche Einschränkung. Spiegelbildlich zu Union, wo unknown alles verschluckt — bei Intersection trägt es schlicht nichts bei.

T & any ist any — any zerstört wie bei Union.

Sobald any Teil einer Intersection ist, fällt das gesamte Ergebnis auf any zurück. Das ist eine der vielen Stellen, an denen any still und leise das Typsystem aushebelt — vermeide es konsequent zu Gunsten von unknown.

Property-Konflikt zwischen unterschiedlichen Typen wird zu never — still und leise.

Wenn zwei Bestandteile dieselbe Property mit kollidierenden Typen tragen, wird die Property zu never. TypeScript meldet das nicht auf der Definition selbst, sondern erst beim Versuch, einen Wert zu konstruieren. Ein klassisches Anti-Pattern, das schwer zu debuggen ist.

Intersection mit kollidierenden Methoden-Signaturen kann zu Overloads werden.

Mehrere Funktions-Typen mit unterschiedlichen Signaturen ergeben in der Intersection einen Typ mit allen Signaturen als Overloads. Das ist ein elegantes Idiom zur Beschreibung polymorpher APIs, ohne explizite function-Overload-Deklarationen.

Mixin-Komposition ist oft sauberer als Vererbung.

Quer-Concerns wie Auditable, Timestamped, SoftDeletable lassen sich orthogonal mit & kombinieren. Eine Vererbungskette würde dieselbe Information in eine künstliche Hierarchie zwingen — Komposition bleibt flach und gut lesbar.

Object.assign(a, b) ist das Runtime-Pendant zu Intersection.

Was A & B auf Typebene beschreibt, leistet Object.assign({}, a, b) oder { ...a, ...b } zur Laufzeit: ein Objekt mit den Eigenschaften aller Quellen. Diese Symmetrie hilft beim mentalen Modell — Intersection ist nicht magisch, sondern beschreibt eine reale JS-Operation.

Style-Empfehlung: kurze Intersections inline, lange via type-Alias.

User & Auditable liest sich problemlos inline in einer Signatur. Sobald drei oder mehr Bestandteile zusammenkommen, lohnt sich ein benannter Type-Alias — der Name dokumentiert die Absicht und macht Fehlermeldungen lesbar. Lange anonyme Intersections sind in Hover-Tooltips schwer zu erfassen.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Union & Intersection

Zur Übersicht