Du modellierst deine Zustände mit diskriminierten Unions und sicherst ihre Vollständigkeit mit dem Typ never ab.
Diese Lektion in Kodokon öffnennever ist der leere Typ: Kein Wert bewohnt ihn. Er taucht auf, wenn der Compiler beweist, dass ein Codepfad unmöglich ist - zum Beispiel, nachdem jedes Mitglied einer Union abgearbeitet wurde. Diese Eigenschaft macht ihn zu einem Entwurfswerkzeug: Wenn du den Compiler dazu bringst, im default-Zweig eines switch never zu sagen, hast du den statischen Beweis, dass jeder Fall behandelt ist. Das ist die Grundlage der Vollständigkeitsprüfung.
Diskriminierte Unions hast du im vorherigen Modul entdeckt; jetzt ist es Zeit, sie zu einem echten Entwurfswerkzeug zu machen. Verglichen mit einem einzigen Interface voller optionaler Felder macht eine diskriminierte Union unmögliche Zustände nicht darstellbar: Du kannst nicht zugleich data und error haben, und auch keinen Erfolg ohne Daten. Es ist das Muster der Wahl für Ladezustände, Ergebnisse von Operationen und Ereignisnachrichten.
type FetchState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };
function render(state: FetchState<string[]>): string {
switch (state.status) {
case "idle": return "Waiting";
case "loading": return "Loading…";
case "success": return state.data.join(", ");
case "error": return state.error.message;
}
}Den assertNever-Guard, der im default-Zweig aufgerufen wird, hast du schon gesehen; sezieren wir jetzt genau, wie er funktioniert. Solange jeder Fall abgedeckt ist, hat sein Parameter dort den Typ never und alles kompiliert. Fügst du der Union eine Variante hinzu, ohne den switch zu aktualisieren, ist die vergessene Variante nicht mehr never zuweisbar: Fehler zur Kompilierzeit, mit dem Namen des fehlenden Falls in der Meldung. Du verwandelst einen Produktionsbug in einen abgelehnten Build.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "rect"; width: number; height: number };
function assertNever(value: never): never {
throw new Error("Unhandled case: " + String(value));
}
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "square": return shape.side ** 2;
case "rect": return shape.width * shape.height;
default: return assertNever(shape);
}
}Zwei fortgeschrittene Feinheiten verdienen deine Aufmerksamkeit. Erstens pflanzt sich die Verengung über das Unterscheidungsmerkmal nur dann in verschachtelte Strukturen fort, wenn du das umgebende Objekt direkt prüfst; das Unterscheidungsmerkmal für sich allein zu destrukturieren zerreißt die Verbindung (abgesehen von den Fällen, die seit TypeScript 4.6 bei gemeinsamer Destrukturierung behandelt werden). Zweitens wird never in Unions absorbiert (string | never ist einfach string): Genau das erlaubt es bedingten Typen, Varianten herauszufiltern, so wie Exclude es tut.
type Event =
| { type: "click"; x: number; y: number }
| { type: "keypress"; key: string };
type EventOf<T extends Event["type"]> =
Extract<Event, { type: T }>;
type ClickEvent = EventOf<"click">;
function handle<T extends Event["type"]>(
type: T,
handler: (event: EventOf<T>) => void
): void {
/* register the handler */
}
handle("click", (e) => console.log(e.x, e.y));Shape eine Variante hinzufügst, ohne den switch mit assertNever zu ändern?string | never?neverstringunknownstring | undefined