Aufwärmen · Aufgabe 1 von 7
// B3.5 · ca. 38 Min. · Einstieg
never und Vollständigkeit; Projekt: ein Zustandsautomat für Bestellungen
Nach dieser Lektion wissen Sie, wo never auftritt, machen einen switch vollständig, lesen die Meldung für ein neues Mitglied und bauen einen Zustandsautomaten, dessen falsche Schritte nicht kompilieren.
Lektion 5 von 5 in B3 Eingrenzung (Narrowing)
Danach können Sie
- Erklären, wo never auftritt, was ihm zuweisbar ist und welche Funktionen nie zurückkehren
- Einen switch mit never-Prüfung oder assertNever vollständig machen und den Fehler für ein neues Mitglied lesen
- Einen Zustandsautomaten als Discriminated Union modellieren, deren Übergänge nur gültige Zustände annehmen
Vorhersagen · Aufgabe 2 von 7
Sagen Sie es vorher, bevor Sie weiterlesen. label hat keinen case für "cancelled" und kein default. Was sagt tsc?
type Pending = { status: "pending"; id: number }; type Paid = { status: "paid"; id: number; paidAt: string }; type Shipped = { status: "shipped"; id: number; paidAt: string; tracking: string }; type Delivered = { status: "delivered"; id: number; paidAt: string; tracking: string; deliveredAt: string }; type Cancelled = { status: "cancelled"; id: number; reason: string }; type Order = Pending | Paid | Shipped | Delivered | Cancelled; function label(order: Order): string { switch (order.status) { case "pending": return "waiting"; case "paid": return "paid"; case "shipped": return "on its way"; case "delivered": return "delivered"; } }Üben · Aufgabe 3 von 7
Setzen Sie den Parametertyp ein, damit assertNever(order) nur kompiliert, wenn jeder Status einen case hat.
function assertNever(value: ____): never { throw new Error("Unexpected order: " + JSON.stringify(value)); }function assertNever(value: ): never {Üben · Aufgabe 4 von 7
Ordnen Sie mit den Bestellzuständen unten jedem Ausdruck seinen Typ zu.
type Pending = { status: "pending"; id: number }; type Paid = { status: "paid"; id: number; paidAt: string }; type Shipped = { status: "shipped"; id: number; paidAt: string; tracking: string }; type Delivered = { status: "delivered"; id: number; paidAt: string; tracking: string; deliveredAt: string }; type Cancelled = { status: "cancelled"; id: number; reason: string }; type Order = Pending | Paid | Shipped | Delivered | Cancelled;Üben · Aufgabe 5 von 7
Order hat einen neuen Status bekommen, Returned (Zeile 6). Welche Zeilen lehnt tsc ab?
type Pending = { status: "pending"; id: number }; type Paid = { status: "paid"; id: number; paidAt: string }; type Shipped = { status: "shipped"; id: number; paidAt: string; tracking: string }; type Cancelled = { status: "cancelled"; id: number; reason: string }; type Returned = { status: "returned"; id: number; refund: number }; type Order = Pending | Paid | Shipped | Cancelled | Returned; function assertNever(value: never): never { throw new Error("Unexpected order: " + JSON.stringify(value)); } function isOpen(order: Order): boolean { switch (order.status) { case "pending": case "paid": case "shipped": return true; case "cancelled": return false; default: return assertNever(order); } } function canCancel(order: Order) { switch (order.status) { case "pending": case "paid": return true; case "shipped": case "cancelled": return false; default: { const unhandled: never = order; return unhandled; } } } function icon(order: Order) { switch (order.status) { case "pending": return "P"; case "paid": return "€"; case "shipped": return "S"; case "cancelled": return "X"; } }Denksport · Aufgabe 6 von 7
Knobelaufgabe. fail ist eine Pfeilfunktion mit dem Rückgabetyp never. Was meldet tsc?
const fail = (msg: string): never => { throw new Error(msg); }; function shout(x: string | undefined): string { if (x === undefined) fail("missing"); return x.toUpperCase(); }Anwenden · Aufgabe 7 von 7
Kleine Aufgabe. Ein Dokument geht vom Entwurf zur Prüfung und zur Veröffentlichung, und ein Prüfer kann es zum Entwurf zurückschicken. Modellieren Sie die drei Phasen als Discriminated Union, schreiben Sie die Übergänge submit, reject und publish so, dass jeder nur die Phase annimmt, von der er ausgeht, und schreiben Sie ein vollständiges nextAction(doc) mit assertNever in default. Fügen Sie dann eine vierte Phase hinzu, archived, und folgen Sie den Fehlern von tsc, bis es wieder kompiliert. 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
Ein Support-Ticket, das nur vorwärts geht
Ein Ticket ist offen, zugewiesen oder gelöst, und jeder Zustand trägt die Daten, die er bis dahin hat. assign, resolve und reopen nehmen nur den Zustand an, von dem sie ausgehen, also würde assign(done, "Leo") nicht kompilieren. describe endet mit assertNever, das zugleich das Sicherheitsnetz für Daten ist, die tsc nie gesehen hat: Das JSON vom "Server" hat einen Zustand, den kein case behandelt. Führen Sie npx tsc aus, dann node main.ts. Fügen Sie danach type Escalated = { state: "escalated"; id: number; title: string } zu Ticket hinzu und führen Sie tsc erneut aus.
main.ts
// A support ticket: open -> assigned -> resolved, and a resolved ticket can be reopened.
type Open = { state: "open"; id: number; title: string };
type Assigned = { state: "assigned"; id: number; title: string; agent: string };
type Resolved = { state: "resolved"; id: number; title: string; agent: string; note: string };
type Ticket = Open | Assigned | Resolved;
function assertNever(value: never): never {
throw new Error("Unexpected ticket: " + JSON.stringify(value));
}
// Each transition accepts only the state it may start from.
function assign(ticket: Open, agent: string): Assigned {
return { ...ticket, state: "assigned", agent };
}
function resolve(ticket: Assigned, note: string): Resolved {
return { ...ticket, state: "resolved", note };
}
function reopen(ticket: Resolved): Open {
return { state: "open", id: ticket.id, title: ticket.title };
}
function describe(ticket: Ticket): string {
const head = "#" + ticket.id + " " + ticket.title + ": ";
switch (ticket.state) {
case "open":
return head + "waiting for an agent";
case "assigned":
return head + ticket.agent + " is on it";
case "resolved":
return head + "resolved by " + ticket.agent + " (" + ticket.note + ")";
default:
return assertNever(ticket); // compiles only while every state has a case
}
}
const opened: Open = { state: "open", id: 7, title: "Login fails" };
const taken = assign(opened, "Mia");
const done = resolve(taken, "password reset");
for (const ticket of [opened, taken, done, reopen(done)]) {
console.log(describe(ticket));
}
// tsc cannot check data that arrives while the program runs:
const fromServer: Ticket = JSON.parse('{"state":"escalated","id":8,"title":"Disk full"}');
try {
console.log(describe(fromServer));
} catch (error) {
console.log(error instanceof Error ? error.message : "unknown error");
}
Ausführen mit
npx tsc
node main.tsAusgabe
#7 Login fails: waiting for an agent
#7 Login fails: Mia is on it
#7 Login fails: resolved by Mia (password reset)
#7 Login fails: waiting for an agent
Unexpected ticket: {"state":"escalated","id":8,"title":"Disk full"}- Mit Escalated in Ticket meldet tsc die assertNever-Zeile: Argument of type 'Escalated' is not assignable to parameter of type 'never'.
- JSON.parse liefert any, also nimmt tsc den Wert ungeprüft als Ticket an; assertNever fängt den unbekannten Zustand zur Laufzeit ab.
- reopen baut ein neues Open-Objekt, statt zu spreaden, damit agent und note nicht ins offene Ticket zurückwandern.
- Der Spread { ...ticket, state: "assigned", agent } behält id und title und ersetzt state.
Ä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 3
Den Status behandeln, den tsc nennt
badge hat eine never-Prüfung in default, und tsc meldet error TS2322: Type 'Cancelled' is not assignable to type 'never'. Ergänzen Sie den fehlenden case, sodass eine stornierte Bestellung "CANCELLED (" gefolgt von ihrem Grund und ")" ergibt. Behalten Sie die never-Prü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
Hinweis 1
Die Meldung nennt das Mitglied, das default erreicht: Cancelled. Welcher case fehlt?
Hinweis 2
In case "cancelled" ist order ein Cancelled, also kompiliert order.reason.
Hinweis 3
Ergänzen Sie vor default: case "cancelled": return "CANCELLED (" + order.reason + ")";
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
export type Pending = { status: "pending"; id: number };
export type Paid = { status: "paid"; id: number; paidAt: string };
export type Shipped = { status: "shipped"; id: number; paidAt: string; tracking: string };
export type Delivered = { status: "delivered"; id: number; paidAt: string; tracking: string; deliveredAt: string };
export type Cancelled = { status: "cancelled"; id: number; reason: string };
export type Order = Pending | Paid | Shipped | Delivered | Cancelled;
export function badge(order: Order): string {
switch (order.status) {
case "pending":
return "PENDING";
case "paid":
return "PAID";
case "shipped":
return "SHIPPED " + order.tracking;
case "delivered":
return "DELIVERED";
case "cancelled":
return "CANCELLED (" + order.reason + ")";
default: {
const unhandled: never = order;
return unhandled;
}
}
}
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 Pending = { status: "pending"; id: number };
export type Paid = { status: "paid"; id: number; paidAt: string };
export type Shipped = { status: "shipped"; id: number; paidAt: string; tracking: string };
export type Delivered = { status: "delivered"; id: number; paidAt: string; tracking: string; deliveredAt: string };
export type Cancelled = { status: "cancelled"; id: number; reason: string };
export type Order = Pending | Paid | Shipped | Delivered | Cancelled;
export function badge(order: Order): string {
switch (order.status) {
case "pending":
return "PENDING";
case "paid":
return "PAID";
case "shipped":
return "SHIPPED " + order.tracking;
case "delivered":
return "DELIVERED";
default: {
const unhandled: never = order;
return unhandled;
}
}
}
main.test.ts
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {badge} from './main.ts';
test('eine stornierte Bestellung zeigt ihren Grund', () => {
const text = badge({status: 'cancelled', id: 4, reason: 'out of stock'});
assert.equal(text, 'CANCELLED (out of stock)', `badge einer stornierten Bestellung ergab ${JSON.stringify(text)}`);
});
test('die anderen Status behalten ihre Badges', () => {
assert.equal(badge({status: 'pending', id: 1}), 'PENDING', 'badge einer offenen Bestellung sollte "PENDING" sein');
assert.equal(badge({status: 'shipped', id: 2, paidAt: '2026-10-04', tracking: 'DHL-7'}), 'SHIPPED DHL-7', 'badge einer versandten Bestellung sollte "SHIPPED DHL-7" sein');
});
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.tsPrüfungen ausführen (learnrun.js muss im selben Ordner liegen):
npx tsc
node --testlearnrun.js herunterladenÜbung 2 von 3
assertNever werfen lassen
nextStep ist vollständig, und tsc nimmt die Datei an. Aber assertNever gibt sein Argument nur zurück, also rutscht eine Bestellung mit einem Status von außerhalb des Programms, etwa "lost" aus JSON, als Objekt durch. Lassen Sie assertNever einen Error werfen, dessen message "Unexpected order: " gefolgt von JSON.stringify(value) ist.
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
Hinweis 1
Eine Funktion mit dem Rückgabetyp never darf nicht normal zurückkehren. return value kompiliert nur, weil value selbst never ist.
Hinweis 2
value.status können Sie nicht lesen: Der Typ never hat keine Properties. JSON.stringify(value) funktioniert für jeden Wert.
Hinweis 3
throw new Error("Unexpected order: " + JSON.stringify(value));
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
export type Pending = { status: "pending"; id: number };
export type Paid = { status: "paid"; id: number; paidAt: string };
export type Shipped = { status: "shipped"; id: number; paidAt: string; tracking: string };
export type Delivered = { status: "delivered"; id: number; paidAt: string; tracking: string; deliveredAt: string };
export type Cancelled = { status: "cancelled"; id: number; reason: string };
export type Order = Pending | Paid | Shipped | Delivered | Cancelled;
export function assertNever(value: never): never {
throw new Error("Unexpected order: " + JSON.stringify(value));
}
export function nextStep(order: Order): string {
switch (order.status) {
case "pending":
return "pay";
case "paid":
return "ship";
case "shipped":
return "deliver";
case "delivered":
case "cancelled":
return "none";
default:
return assertNever(order);
}
}
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 Pending = { status: "pending"; id: number };
export type Paid = { status: "paid"; id: number; paidAt: string };
export type Shipped = { status: "shipped"; id: number; paidAt: string; tracking: string };
export type Delivered = { status: "delivered"; id: number; paidAt: string; tracking: string; deliveredAt: string };
export type Cancelled = { status: "cancelled"; id: number; reason: string };
export type Order = Pending | Paid | Shipped | Delivered | Cancelled;
export function assertNever(value: never): never {
return value;
}
export function nextStep(order: Order): string {
switch (order.status) {
case "pending":
return "pay";
case "paid":
return "ship";
case "shipped":
return "deliver";
case "delivered":
case "cancelled":
return "none";
default:
return assertNever(order);
}
}
main.test.ts
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {nextStep} from './main.ts';
test('jeder bekannte Status hat einen nächsten Schritt', () => {
assert.equal(nextStep({status: 'pending', id: 1}), 'pay', 'nextStep einer offenen Bestellung sollte "pay" sein');
assert.equal(nextStep({status: 'paid', id: 1, paidAt: '2026-10-04'}), 'ship', 'nextStep einer bezahlten Bestellung sollte "ship" sein');
assert.equal(nextStep({status: 'cancelled', id: 1, reason: 'late'}), 'none', 'nextStep einer stornierten Bestellung sollte "none" sein');
});
test('ein unbekannter Status aus JSON wirft mit der Bestellung in der message', () => {
assert.throws(() => nextStep(JSON.parse('{"status":"lost","id":9}')), {message: 'Unexpected order: {"status":"lost","id":9}'}, 'nextStep sollte einen Error mit der message "Unexpected order: " plus der Bestellung als JSON werfen');
});
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.tsPrüfungen ausführen (learnrun.js muss im selben Ordner liegen):
npx tsc
node --testlearnrun.js herunterladenÜbung 3 von 3
Projekt: ein Zustandsautomat für Bestellungen
Bauen Sie den Zustandsautomaten für Bestellungen. Schritt 1: Geben Sie pay, ship, deliver und cancel die engsten Parametertypen, sodass nur gültige Schritte kompilieren: pay geht von pending aus, ship von paid, deliver von shipped und cancel von pending oder paid. Die drei // @ts-expect-error-Zeilen in forbiddenMoves prüfen das: tsc meldet sie, solange ein falscher Schritt noch kompiliert. Schritt 2: Schreiben Sie describe, mit einem case pro Status und default: return assertNever(order). Die Tests zeigen die genauen Texte.
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
Hinweis 1
Beginnen Sie mit den Fehlern von tsc. Mit order: Order gibt es order.paidAt nicht bei jedem Mitglied, und ship(pending, …) kompiliert, also ist sein @ts-expect-error unbenutzt.
Hinweis 2
Der Parametertyp ist der Zustand, von dem ein Schritt ausgeht: pay(order: Pending, …), ship(order: Paid, …), deliver(order: Shipped, …). cancel nimmt eine Union aus zwei Zuständen.
Hinweis 3
In describe: switch (order.status) mit fünf cases; in case "paid" dürfen Sie order.paidAt lesen, in case "cancelled" order.reason.
Hinweis 4
Jeder case liefert "Order " + order.id + ": " gefolgt von seinem Text, etwa "shipped, tracking " + order.tracking. default: return assertNever(order);
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
export type Pending = { status: "pending"; id: number };
export type Paid = { status: "paid"; id: number; paidAt: string };
export type Shipped = { status: "shipped"; id: number; paidAt: string; tracking: string };
export type Delivered = { status: "delivered"; id: number; paidAt: string; tracking: string; deliveredAt: string };
export type Cancelled = { status: "cancelled"; id: number; reason: string };
export type Order = Pending | Paid | Shipped | Delivered | Cancelled;
export function assertNever(value: never): never {
throw new Error("Unexpected order: " + JSON.stringify(value));
}
// Step 1: give each transition the narrowest parameter type.
export function pay(order: Pending, paidAt: string): Paid {
return { status: "paid", id: order.id, paidAt };
}
export function ship(order: Paid, tracking: string): Shipped {
return { status: "shipped", id: order.id, paidAt: order.paidAt, tracking };
}
export function deliver(order: Shipped, deliveredAt: string): Delivered {
return { status: "delivered", id: order.id, paidAt: order.paidAt, tracking: order.tracking, deliveredAt };
}
export function cancel(order: Pending | Paid, reason: string): Cancelled {
return { status: "cancelled", id: order.id, reason };
}
// Step 2: describe every status, with assertNever in default.
export function describe(order: Order): string {
const head = "Order " + order.id + ": ";
switch (order.status) {
case "pending":
return head + "waiting for payment";
case "paid":
return head + "paid on " + order.paidAt;
case "shipped":
return head + "shipped, tracking " + order.tracking;
case "delivered":
return head + "delivered on " + order.deliveredAt;
case "cancelled":
return head + "cancelled (" + order.reason + ")";
default:
return assertNever(order);
}
}
// These moves must not compile. Keep them exactly as they are.
export function forbiddenMoves(pending: Pending, shipped: Shipped, delivered: Delivered): void {
// @ts-expect-error: an order must be paid before it is shipped
ship(pending, "DHL-1");
// @ts-expect-error: a shipped order can no longer be cancelled
cancel(shipped, "changed my mind");
// @ts-expect-error: a delivered order cannot be delivered again
deliver(delivered, "2026-10-09");
}
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 Pending = { status: "pending"; id: number };
export type Paid = { status: "paid"; id: number; paidAt: string };
export type Shipped = { status: "shipped"; id: number; paidAt: string; tracking: string };
export type Delivered = { status: "delivered"; id: number; paidAt: string; tracking: string; deliveredAt: string };
export type Cancelled = { status: "cancelled"; id: number; reason: string };
export type Order = Pending | Paid | Shipped | Delivered | Cancelled;
export function assertNever(value: never): never {
throw new Error("Unexpected order: " + JSON.stringify(value));
}
// Step 1: give each transition the narrowest parameter type.
export function pay(order: Order, paidAt: string): Paid {
return { status: "paid", id: order.id, paidAt };
}
export function ship(order: Order, tracking: string): Shipped {
return { status: "shipped", id: order.id, paidAt: order.paidAt, tracking };
}
export function deliver(order: Order, deliveredAt: string): Delivered {
return { status: "delivered", id: order.id, paidAt: order.paidAt, tracking: order.tracking, deliveredAt };
}
export function cancel(order: Order, reason: string): Cancelled {
return { status: "cancelled", id: order.id, reason };
}
// Step 2: describe every status, with assertNever in default.
export function describe(order: Order): string {
throw new Error("describe is not written yet");
}
// These moves must not compile. Keep them exactly as they are.
export function forbiddenMoves(pending: Pending, shipped: Shipped, delivered: Delivered): void {
// @ts-expect-error: an order must be paid before it is shipped
ship(pending, "DHL-1");
// @ts-expect-error: a shipped order can no longer be cancelled
cancel(shipped, "changed my mind");
// @ts-expect-error: a delivered order cannot be delivered again
deliver(delivered, "2026-10-09");
}
main.test.ts
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {pay, ship, deliver, cancel, describe} from './main.ts';
const pending = {status: 'pending', id: 1};
const paid = {status: 'paid', id: 1, paidAt: '2026-10-04'};
const shipped = {status: 'shipped', id: 1, paidAt: '2026-10-04', tracking: 'DHL-1'};
const delivered = {status: 'delivered', id: 1, paidAt: '2026-10-04', tracking: 'DHL-1', deliveredAt: '2026-10-06'};
test('pay macht aus einer offenen Bestellung eine bezahlte', () => {
assert.deepEqual(pay(pending, '2026-10-04'), paid, 'pay(pending, "2026-10-04") sollte die bezahlte Bestellung liefern');
});
test('ship behält das Zahlungsdatum und ergänzt die Sendungsnummer', () => {
assert.deepEqual(ship(paid, 'DHL-1'), shipped, 'ship(paid, "DHL-1") sollte die versandte Bestellung liefern');
});
test('deliver behält die Sendungsnummer und ergänzt das Lieferdatum', () => {
assert.deepEqual(deliver(shipped, '2026-10-06'), delivered, 'deliver(shipped, "2026-10-06") sollte die zugestellte Bestellung liefern');
});
test('cancel liefert eine stornierte Bestellung mit dem Grund', () => {
assert.deepEqual(cancel(paid, 'out of stock'), {status: 'cancelled', id: 1, reason: 'out of stock'}, 'cancel(paid, "out of stock") sollte eine stornierte Bestellung liefern');
});
test('describe beschreibt jeden Status', () => {
assert.equal(describe(pending), 'Order 1: waiting for payment', 'describe der offenen Bestellung stimmt nicht');
assert.equal(describe(paid), 'Order 1: paid on 2026-10-04', 'describe der bezahlten Bestellung stimmt nicht');
assert.equal(describe(shipped), 'Order 1: shipped, tracking DHL-1', 'describe der versandten Bestellung stimmt nicht');
assert.equal(describe(delivered), 'Order 1: delivered on 2026-10-06', 'describe der zugestellten Bestellung stimmt nicht');
assert.equal(describe({status: 'cancelled', id: 2, reason: 'out of stock'}), 'Order 2: cancelled (out of stock)', 'describe der stornierten Bestellung stimmt nicht');
});
test('describe wirft bei einem unbekannten Status', () => {
assert.throws(() => describe(JSON.parse('{"status":"lost","id":9}')), /Unexpected order/, 'describe sollte bei Status "lost" "Unexpected order: ..." werfen');
});
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.tsPrüfungen ausführen (learnrun.js muss im selben Ordner liegen):
npx tsc
node --testlearnrun.js herunterladenHäufige Fehler
Eine never-Funktion, die zurückkehren kann
function fail(message: string): never {
console.log("Error: " + message);
}
fail("payment declined");
Was tsc oder Node.js ausgibt
main.ts(1,33): error TS2534: A function returning 'never' cannot have a reachable end point.Warum, und die Lösung
Der Rückgabetyp never verspricht, dass die Funktion nicht zurückkommt, also kann Code nach ihrem Aufruf als unerreichbar gelten. Dieser Rumpf gibt etwas aus und endet dann normal. Werfen Sie stattdessen: throw new Error(message);. Soll die Funktion wirklich zurückkehren, geben Sie ihr den Rückgabetyp void.
Ein vollständiger switch ohne never-Prüfung
type Pending = { status: "pending"; id: number };
type Paid = { status: "paid"; id: number; paidAt: string };
type Shipped = { status: "shipped"; id: number; paidAt: string; tracking: string };
type Delivered = { status: "delivered"; id: number; paidAt: string; tracking: string; deliveredAt: string };
type Cancelled = { status: "cancelled"; id: number; reason: string };
type Order = Pending | Paid | Shipped | Delivered | Cancelled;
function label(order: Order): string {
switch (order.status) {
case "pending":
return "waiting";
case "paid":
return "paid";
case "shipped":
return "on its way";
case "delivered":
return "delivered";
}
}
console.log(label({ status: "pending", id: 1 }));
Was tsc oder Node.js ausgibt
main.ts(8,31): error TS2366: Function lacks ending return statement and return type does not include 'undefined'.Warum, und die Lösung
Eine stornierte Bestellung fällt aus dem switch heraus, und der deklarierte Rückgabetyp string erlaubt das nicht. tsc findet die Lücke, nennt sie aber nicht. Ergänzen Sie den case und dazu default: return assertNever(order); (oder const unhandled: never = order), damit der nächste fehlende Status mit Namen gemeldet wird, an der Stelle, die ihn behandeln muss.
Ein Übergang, der jeden Zustand annimmt
type Pending = { status: "pending"; id: number };
type Paid = { status: "paid"; id: number; paidAt: string };
type Shipped = { status: "shipped"; id: number; paidAt: string; tracking: string };
type Delivered = { status: "delivered"; id: number; paidAt: string; tracking: string; deliveredAt: string };
type Cancelled = { status: "cancelled"; id: number; reason: string };
type Order = Pending | Paid | Shipped | Delivered | Cancelled;
function cancel(order: Order, reason: string): Cancelled {
return { status: "cancelled", id: order.id, reason };
}
function forbiddenMoves(shipped: Shipped): void {
// @ts-expect-error: a shipped order can no longer be cancelled
cancel(shipped, "changed my mind");
}
Was tsc oder Node.js ausgibt
main.ts(13,3): error TS2578: Unused '@ts-expect-error' directive.Warum, und die Lösung
Die Direktive sagt, dass die nächste Zeile ein Kompilierfehler sein muss, und das ist sie nicht: cancel nimmt jede Order, also kompiliert das Stornieren einer versandten Bestellung. Grenzen Sie den Parameter auf die Zustände ein, von denen ein Storno ausgehen darf: function cancel(order: Pending | Paid, reason: string): Cancelled. Dann ist der Aufruf ein Fehler, die Direktive verbraucht ihn, und tsc ist still.
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.