Kodokon kodokon.com

never, Vollständigkeit und fortgeschrittene diskriminierte Unions

Du modellierst deine Zustände mit diskriminierten Unions und sicherst ihre Vollständigkeit mit dem Typ never ab.

10 Min. · 3 Fragen

Diese Lektion in Kodokon öffnen

never 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.

TYPESCRIPT
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;
  }
}
data existiert nur in der Erfolgsvariante: kein Zugriff an anderer Stelle.

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.

TYPESCRIPT
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);
  }
}
Eine Variante zu Shape hinzuzufügen bricht die Kompilierung von area.

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.

TYPESCRIPT
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));
Extract filtert die Union über das Unterscheidungsmerkmal: Der Handler ist präzise.

Wissenscheck

Stelle sicher, dass du die wichtigsten Punkte dieser Lektion behalten hast.

  1. Warum ist eine diskriminierte Union für einen Ladezustand besser als ein Interface mit optionalen Feldern?
    • Sie verbraucht zur Laufzeit weniger Speicher
    • Sie macht widersprüchliche Kombinationen (data und error gleichzeitig) nicht darstellbar
    • Sie erspart das Schreiben von switch-Anweisungen
    • Sie ist im Strict-Modus vorgeschrieben
  2. Was passiert, wenn du Shape eine Variante hinzufügst, ohne den switch mit assertNever zu ändern?
    • Bei jedem Aufruf von area wird eine Ausnahme geworfen
    • Der Code kompiliert, aber die neue Variante gibt undefined zurück
    • Die Kompilierung schlägt fehl, weil die vergessene Variante nicht never zuweisbar ist
  3. Welcher Typ ist string | never?
    • never
    • string
    • unknown
    • string | undefined