Zum Inhalt springen
aviral gupta

// B5.3 · ca. 30 Min. · Einstieg

Typen erweitern und Intersections

Nach dieser Lektion bauen Sie Typen mit extends und & aus anderen Typen auf, sagen voraus, was bei Konflikten passiert, und wählen das Passende.

Lektion 3 von 5 in B5 Objekttypen

Danach können Sie

  • Ein Interface mit extends auf einem oder mehreren anderen aufbauen und eine geerbte Eigenschaft spezieller machen
  • Typen mit & kombinieren und den Typ einer Eigenschaft vorhersagen, die beide Seiten deklarieren
  • Erklären, wo extends und & einen Konflikt melden, und zwischen beiden wählen
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen mit JavaScript: Zwei Objekte mit demselben Schlüssel werden in eines gespreadet. Was gibt das aus?

    const base = { id: 1, status: "open" };
    const extra = { status: 404, owner: "Ada" };
    console.log({ ...base, ...extra });
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es voraus, bevor Sie weiterlesen: Staff kombiniert zwei Typen, die sich bei name widersprechen. Was meldet tsc?

    interface Person1 {
      name: string;
    }
    
    interface Person2 {
      name: number;
    }
    
    type Staff = Person1 & Person2;
    
    const ada: Staff = { name: "Ada" };
  3. Üben · Aufgabe 3 von 7

    Ein Entwurf hat vielleicht noch keinen Titel, ein veröffentlichter Beitrag muss einen haben. Ergänzen Sie den Member, den Published neu deklariert, sodass headline kompiliert und das Programm HELLO ausgibt.

    interface Draft {
      title?: string;
      body: string;
    }
    
    interface Published extends Draft {
      ____;
    }
    
    function headline(post: Published): string {
      return post.title.toUpperCase();
    }
    
    console.log(headline({ title: "Hello", body: "First post" }));
    ;
  4. Üben · Aufgabe 4 von 7

    Offer erweitert zwei Interfaces, die sich beim Preis widersprechen. Was meldet tsc?

    interface Product {
      price: number;
    }
    
    interface Listing {
      price: string;
    }
    
    interface Offer extends Product, Listing {}
  5. Üben · Aufgabe 5 von 7

    Beide Typen deklarieren port. Welchen Typ hat port am Kommentar?

    type Defaults = { port: number; host: string };
    type Secure = { port: 443 | 8443 };
    
    function connect(config: Defaults & Secure) {
      const port = config.port;
      // here
      return config.host + ":" + port;
    }
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Ein Haustier, das zugleich Cat und Dog ist: Was meldet tsc?

    type Cat = { kind: "cat"; lives: number };
    type Dog = { kind: "dog"; bark: string };
    type CatDog = Cat & Dog;
    
    function describe(pet: CatDog) {
      return pet.lives;
    }
  7. Anwenden · Aufgabe 7 von 7

    Kleine Aufgabe. Schreiben Sie ein Interface Contact mit einem name und einer optionalen email sowie ein Interface Customer, das es erweitert, email zur Pflicht macht und eine customerNo (eine Zahl) hinzufügt. Schreiben Sie card(c), deren Parameter ein Customer ist, mit & kombiniert mit { note: string }, und die einen Text wie Ada <ada@example.com> #17: prefers email zurückgibt. Rufen Sie sie einmal auf. Lassen Sie dann Customer email: number deklarieren und lesen Sie den Fehler, und probieren Sie auch { note: number } & { note: string } aus. Prüfen Sie es im Editor oder mit npx tsc und node main.ts.

    Prüfen Sie Ihr Ergebnis anhand dieser Liste

Selbst programmieren

Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.

Ausgearbeitetes Beispiel

Tickets, die aufeinander aufbauen

Bug erweitert Ticket, fügt severity hinzu und verengt status auf zwei Werte. AssignedBug erweitert zwei Interfaces zugleich. line kombiniert AssignedBug direkt im Parameter per & mit Timestamped. shortLine nimmt jedes Ticket, also passt auch ein Bug. Closed bildet die Intersection von Ticket mit einem engeren status. Führen Sie npx tsc aus, dann node main.ts. Geben Sie Bug dann den Member status: number, oder ändern Sie Timestamped zu { createdAt: number }, und vergleichen Sie, wo tsc jeweils meldet.

main.ts

// The fields every ticket has.
interface Ticket {
  readonly id: number;
  title: string;
  status: string;
}

// extends adds severity, and makes status more specific: still a string.
interface Bug extends Ticket {
  status: "open" | "fixed";
  severity: 1 | 2 | 3;
}

// Several bases at once.
interface Owned {
  owner: string;
}
interface AssignedBug extends Bug, Owned {}

// & combines types where you need them, without a new interface.
type Timestamped = { createdAt: string };

function line(bug: AssignedBug & Timestamped): string {
  return "#" + bug.id + " " + bug.title + " [" + bug.status + ", sev " + bug.severity + "] " + bug.owner + ", " + bug.createdAt;
}

// Any Bug is also a Ticket, so it fits where a Ticket is expected.
function shortLine(ticket: Ticket): string {
  return "#" + ticket.id + " " + ticket.status;
}

const crash: AssignedBug & Timestamped = {
  id: 7,
  title: "Crash on save",
  status: "open",
  severity: 1,
  owner: "Ada",
  createdAt: "2026-10-04"
};

console.log(line(crash));
console.log(shortLine(crash));

// Both sides declare status: the intersection keeps what fits both.
type Closed = Ticket & { status: "fixed" | "wontfix" };
const old: Closed = { id: 3, title: "Typo in footer", status: "wontfix" };
console.log(shortLine(old));

Ausführen mit

npx tsc
node main.ts

Ausgabe

#7 Crash on save [open, sev 1] Ada, 2026-10-04
#7 open
#3 wontfix
  • status: "open" | "fixed" passt zu status: string, also darf Bug die Eigenschaft neu deklarieren.
  • crash ist ein AssignedBug & Timestamped und passt trotzdem zu shortLine, das nur ein Ticket will.
  • In Closed ist status "fixed" | "wontfix": der Teil von string, den beide Seiten erlauben.
  • status: number in Bug wäre error TS2430 bei Bug; createdAt: number in Timestamped zeigte sich erst bei crash, als never.
Ändern und ausführen

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den TypeScript-Compiler herunter (bis zu 2.1 MB) und speichert ihn im Cache. Ihr Code bleibt auf Ihrem Gerät.

Übungen

Übung 1 von 2

Admins sind Konten mit mehr Regeln

Admin erweitert Account, deklariert aber plan: number, also meldet tsc error TS2430: Interface 'Admin' incorrectly extends interface 'Account'. Der plan eines Admins ist immer "team" oder "enterprise": Deklarieren Sie plan neu mit dieser Literal-Union, die zum string von Account passt. Die Zeile mit // @ts-expect-error findet dann weiterhin ihren Fehler, weil "free" nicht erlaubt ist. Lassen Sie danach canDelete zurückgeben, ob die permissions des Admins "delete" enthalten. Führen Sie die Tests aus: Der erste ist die Typprüfung.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den TypeScript-Compiler herunter (bis zu 2.1 MB) und speichert ihn im Cache. Ihr Code bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Ein Interface, das ein anderes erweitert, darf eine geerbte Eigenschaft neu deklarieren, wenn der neue Typ zum alten passt.

  2. Hinweis 2

    "team" | "enterprise" ist eine Union von String-Literalen und passt daher zu string: plan: "team" | "enterprise";

  3. Hinweis 3

    Arrays haben includes: return admin.permissions.includes("delete");

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

export interface Account {
  readonly id: number;
  email: string;
  plan: string;
}

export interface Admin extends Account {
  plan: "team" | "enterprise";
  permissions: string[];
}

export function summary(admin: Admin): string {
  return admin.email + " (" + admin.plan + ", " + admin.permissions.length + " permissions)";
}

export function canDelete(admin: Admin): boolean {
  return admin.permissions.includes("delete");
}

// @ts-expect-error: an admin's plan is "team" or "enterprise"
const wrong: Admin = { id: 9, email: "eve@example.com", plan: "free", permissions: [] };
Auf dem eigenen Computer ausführen

Installieren Sie TypeScript 7.0 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.ts

export interface Account {
  readonly id: number;
  email: string;
  plan: string;
}

export interface Admin extends Account {
  plan: number;
  permissions: string[];
}

export function summary(admin: Admin): string {
  return admin.email + " (" + admin.plan + ", " + admin.permissions.length + " permissions)";
}

export function canDelete(admin: Admin): boolean {
  return false;
}

// @ts-expect-error: an admin's plan is "team" or "enterprise"
const wrong: Admin = { id: 9, email: "eve@example.com", plan: "free", permissions: [] };

main.test.ts

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {summary, canDelete} from './main.ts';

const ada = {id: 1, email: 'ada@example.com', plan: 'team' as const, permissions: ['read', 'delete']};
const bob = {id: 2, email: 'bob@example.com', plan: 'enterprise' as const, permissions: ['read']};

test('summary zeigt die E-Mail, den Plan und die Zahl der Berechtigungen', () => {
  const got = summary(ada);
  assert.equal(got, 'ada@example.com (team, 2 permissions)', `summary(ada) ergab ${JSON.stringify(got)}`);
});

test('ein Admin mit der Berechtigung delete darf löschen', () => {
  assert.equal(canDelete(ada), true, 'canDelete(ada) sollte true sein: ada hat "delete"');
});

test('ein Admin ohne sie darf es nicht', () => {
  assert.equal(canDelete(bob), false, 'canDelete(bob) sollte false sein: bob hat nur "read"');
});

package.json

{
  "type": "module"
}

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "lib": [
      "esnext",
      "dom"
    ],
    "types": [],
    "strict": true,
    "noEmit": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "allowImportingTsExtensions": true
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "**/*.test.ts"
  ]
}

npx tsc braucht TypeScript im Ordner: Führen Sie dort einmal npm install --save-dev typescript aus. tsc prüft die Typen; Node.js führt main.ts aus, indem es sie entfernt.

Programm ausführen:

npx tsc
node main.ts

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

npx tsc
node --test
learnrun.js herunterladen

Übung 2 von 2

Ein Dokumenttyp mit verstecktem Konflikt

Doc kombiniert drei Typen mit &. Timestamped sagt, createdAt ist ein String, Versioned sagt, eine Zahl, also ist createdAt never, und notes ist error TS2322: Type 'string' is not assignable to type 'never'. Ersetzen Sie in Versioned createdAt durch savedAt, einen String: wann die aktuelle Version gespeichert wurde. Geben Sie notes das savedAt 2026-10-03. label muss Notes v2 (created 2026-10-01, saved 2026-10-03) zurückgeben und bump(doc, savedAt) ein neues Doc mit um eins erhöhter Version und dem übergebenen savedAt.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den TypeScript-Compiler herunter (bis zu 2.1 MB) und speichert ihn im Cache. Ihr Code bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Der Fehler steht bei notes, aber die Ursache liegt in den Typen: Welche Eigenschaft deklarieren Timestamped und Versioned beide?

  2. Hinweis 2

    type Versioned = { version: number; savedAt: string }; dann savedAt: "2026-10-03" zu notes hinzufügen.

  3. Hinweis 3

    Setzen Sie in bump savedAt in den Spread hinter version: { ...doc, version: doc.version + 1, savedAt }

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

export type Titled = { title: string };
export type Timestamped = { createdAt: string };
export type Versioned = { version: number; savedAt: string };

export type Doc = Titled & Timestamped & Versioned;

export function label(doc: Doc): string {
  return doc.title + " v" + doc.version + " (created " + doc.createdAt + ", saved " + doc.savedAt + ")";
}

export function bump(doc: Doc, savedAt: string): Doc {
  return { ...doc, version: doc.version + 1, savedAt };
}

export const notes: Doc = { title: "Notes", version: 2, createdAt: "2026-10-01", savedAt: "2026-10-03" };
Auf dem eigenen Computer ausführen

Installieren Sie TypeScript 7.0 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.ts

export type Titled = { title: string };
export type Timestamped = { createdAt: string };
export type Versioned = { version: number; createdAt: number };

export type Doc = Titled & Timestamped & Versioned;

export function label(doc: Doc): string {
  return doc.title + " v" + doc.version + " (created " + doc.createdAt + ")";
}

export function bump(doc: Doc, savedAt: string): Doc {
  return { ...doc, version: doc.version + 1 };
}

export const notes: Doc = { title: "Notes", version: 2, createdAt: "2026-10-01" };

main.test.ts

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {label, bump, notes} from './main.ts';

test('label zeigt die Version und wann das Dokument erstellt und gespeichert wurde', () => {
  const got = label(notes);
  assert.equal(got, 'Notes v2 (created 2026-10-01, saved 2026-10-03)', `label(notes) ergab ${JSON.stringify(got)}`);
});

test('bump erhöht die Version und hält fest, wann gespeichert wurde', () => {
  const got = bump(notes, '2026-10-04');
  assert.deepEqual(
    got,
    {title: 'Notes', version: 3, createdAt: '2026-10-01', savedAt: '2026-10-04'},
    `bump(notes, '2026-10-04') ergab ${JSON.stringify(got)}`
  );
});

test('bump lässt das ursprüngliche Dokument unverändert', () => {
  bump(notes, '2026-10-04');
  assert.equal(notes.version, 2, `notes.version ist nach bump ${notes.version}, aber bump sollte ein neues Dokument zurückgeben`);
});

package.json

{
  "type": "module"
}

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "lib": [
      "esnext",
      "dom"
    ],
    "types": [],
    "strict": true,
    "noEmit": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "allowImportingTsExtensions": true
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "**/*.test.ts"
  ]
}

npx tsc braucht TypeScript im Ordner: Führen Sie dort einmal npm install --save-dev typescript aus. tsc prüft die Typen; Node.js führt main.ts aus, indem es sie entfernt.

Programm ausführen:

npx tsc
node main.ts

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

npx tsc
node --test
learnrun.js herunterladen

Häufige Fehler

Eine geerbte Eigenschaft optional machen

interface Article {
  title: string;
  body: string;
}

interface Preview extends Article {
  body?: string;
}

Was tsc oder Node.js ausgibt

main.ts(6,11): error TS2430: Interface 'Preview' incorrectly extends interface 'Article'.

Warum, und die Lösung

Eine Preview muss immer noch ein Article sein, und ein Article hat immer einen body. body?: string erlaubt undefined, string aber nicht, also nennt tsc die Eigenschaft in den nächsten Zeilen: Types of property 'body' are incompatible. extends kann eine Eigenschaft nur spezieller machen. Hat eine Vorschau wirklich keinen body, erweitern Sie Article nicht: Deklarieren Sie den gemeinsamen Teil als eigenes Interface, etwa interface Titled { title: string }, und lassen Sie beide dieses erweitern.

Ein Konflikt in einer Intersection, weit weg von seiner Ursache gefunden

type Product = { name: string; price: number };
type Listing = { name: string; price: string };
type Offer = Product & Listing;

const offer: Offer = { name: "Lamp", price: 25 };

Was tsc oder Node.js ausgibt

main.ts(5,38): error TS2322: Type 'number' is not assignable to type 'never'.

Warum, und die Lösung

tsc akzeptiert type Offer, denn & meldet nie einen Konflikt. price muss zugleich eine Zahl und ein String sein, also ist sein Typ never, und jeder Wert scheitert dort, wo er entsteht. Sehen Sie never in einer Meldung über eine Eigenschaft, schauen Sie sich die Typen an, die mit & kombiniert wurden. Entscheiden Sie, welcher Typ stimmt, und ändern Sie den anderen, oder schreiben Sie die Typen als Interfaces mit extends, damit tsc den Konflikt bei der Deklaration meldet.

Eine Union erweitern

type Shape = { kind: "circle"; radius: number } | { kind: "square"; side: number };

interface Tagged extends Shape {
  id: string;
}

Was tsc oder Node.js ausgibt

main.ts(3,26): error TS2312: An interface can only extend an object type or intersection of object types with statically known members.

Warum, und die Lösung

Ein Interface braucht eine bekannte Menge von Membern, und eine Union besteht aus mehreren Formen. Nehmen Sie stattdessen &: type Tagged = Shape & { id: string }. Das ist ein Kreis mit id oder ein Quadrat mit id, und das Verengen über kind funktioniert weiterhin.

TypeScript im Browser: der Compiler von TypeScript 6.0.3, Apache-2.0, danach die eigene Engine Ihres Browsers. Lizenz und Quellcode

Abschlussquiz

5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.

Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

extends darf eine Eigenschaft verengen, ihr aber nie widersprechen

B2.4 hat gezeigt, dass interface Bug extends Ticket die Member von Ticket in Bug übernimmt. Bug darf einen davon auch neu deklarieren, solange der neue Typ zum alten passt: Aus status: string kann status: "open" | "fixed" werden, aus einem optionalen title?: string ein Pflichtfeld title: string. Jeder Bug ist dann immer noch ein Ticket und passt überall, wo ein Ticket erwartet wird. Die umgekehrte Richtung ist error TS2430: Interface 'Bug' incorrectly extends interface 'Ticket'., gefolgt von Zeilen, die die Eigenschaft nennen. Bei mehreren Basen, extends A, B, müssen diese sich über gemeinsame Eigenschaften einig sein, sonst meldet tsc TS2320: Interface 'C' cannot simultaneously extend types 'A' and 'B'. Ein Interface kann einen Objekttyp-Alias erweitern, aber keine Union: TS2312.

& bildet die Schnittmenge der Typen gemeinsamer Eigenschaften

A & B hat jede Eigenschaft von A und von B, und die Prüfung auf überzählige Eigenschaften gilt weiterhin. Deklarieren beide Seiten dieselbe Eigenschaft, wählt & nicht eine aus, wie es ein Spread täte: Die Eigenschaft bekommt beide Typen zugleich. { port: number } & { port: 443 | 8443 } gibt port den Typ 443 | 8443. Können sich die Typen nicht überschneiden, ist das Ergebnis never, ein Typ ohne Werte: In { name: string } & { name: number } ist name never, und { name: "Ada" } ist error TS2322: Type 'string' is not assignable to type 'never'. Widersprechen sich Literaltypen, etwa kind: "cat" und kind: "dog", reduziert tsc die ganze Intersection auf never.

Wo ein Konflikt auftaucht, und was Sie wählen

Das Handbook: „The principal difference between the two is how conflicts are handled“. extends prüft das neue Interface dort, wo Sie es deklarieren, gegen seine Basen; ein Konflikt wird also in der Interface-Zeile gemeldet. & beschwert sich nie beim Typalias: Der Konflikt wird zu never und zeigt sich erst später, wo ein Wert entsteht oder eine Eigenschaft benutzt wird, oft weit entfernt und mit einer rätselhaften Meldung wie Property 'lives' does not exist on type 'never'. Nehmen Sie also interface … extends für benannte Typen, die aufeinander aufbauen und bei denen Sie diese frühe Prüfung wollen. Nehmen Sie &, um Typen dort zu kombinieren, wo Sie sie verwenden, oder wenn eine Seite kein Interface ist, etwa eine Union.

Quellen

Zuletzt geprüft am 4. Oktober 2026