Kodokon kodokon.com

Bedingte Typen und infer: die Logik des Compilers

Meistere die Distributivität bedingter Typen, das Pattern Matching mit infer und Grenzfälle wie never, any und boolean.

10 Min. · 3 Fragen

Diese Lektion in Kodokon öffnen

Bedingte Typen sind der Verzweigungsmechanismus des Typsystems: T extends U ? X : Y fragt den Compiler, ob T zu U zuweisbar ist, und wählt dann einen Zweig aus. Alle Standard-Hilfstypen wie ReturnType, Exclude oder Awaited bauen darauf auf. Auf diesem Niveau lautet die Frage nicht mehr, wie du sie schreibst, sondern wie du genau vorhersagst, auf welche Weise der Compiler sie auswertet, besonders wenn Unions und never im Spiel sind.

TYPESCRIPT
type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">;
// true

type B = IsString<string | number>;
// boolean, not false!
Ein überraschendes Ergebnis bei einer Union: boolean.

Das entscheidende Verhalten ist die Distributivität: Wenn der geprüfte Typ ein nackter Typparameter ist (nicht in ein Tupel, ein Array oder eine Intersection verpackt), wird die Bedingung getrennt auf jedes Mitglied der Union angewendet, und die Ergebnisse werden anschließend wieder zusammengeführt. IsString<string | number> berechnet also true | false, und das ist boolean. Um diese Verteilung abzuschalten, verpackst du beide Seiten in ein Tupel: [T] extends [U]. Ein gefürchteter Grenzfall: never ist die leere Union; die Verteilung über null Mitglieder wertet überhaupt keinen Zweig aus, und das Ergebnis ist never, ganz gleich welchen Zweig du geschrieben hast.

TYPESCRIPT
type ToArray<T> = T extends unknown ? T[] : never;
type NoDist<T> = [T] extends [unknown] ? T[] : never;

type A = ToArray<string | number>;
// string[] | number[]

type B = NoDist<string | number>;
// (string | number)[]

type IsNever<T> = [T] extends [never] ? true : false;
type C = IsNever<never>; // true thanks to the tuple
Das Tupel neutralisiert die Verteilung und macht never zuverlässig.

Das Schlüsselwort infer führt eine Typvariable ein, die per Pattern Matching eingefangen wird: Der Compiler versucht, die Struktur des geprüften Typs mit dem Muster abzugleichen, und bindet die Variable, wenn der Abgleich gelingt. Du kannst mehrere infer-Deklarationen mit demselben Namen platzieren: An einer kovarianten Position (erzeugte Werte, etwa ein Rückgabetyp) werden die Kandidaten zu einer Union verschmolzen; an einer kontravarianten Position (Funktionsparameter) werden sie zu einer Intersection kombiniert. Diese Regel folgt direkt aus der Varianz: Sie ist in jeder Richtung die einzig sichere Wahl.

TYPESCRIPT
type ElementOf<T> =
  T extends readonly (infer E)[] ? E : never;

type FirstParam<T> =
  T extends (arg: infer P) => unknown ? P : never;

type Merged<T> =
  T extends { a: (x: infer P) => void;
              b: (x: infer P) => void }
    ? P : never;

type M = Merged<{
  a: (x: { id: string }) => void;
  b: (x: { tag: string }) => void;
}>; // { id: string } & { tag: string }
Zwei infer an kontravarianter Position: Intersection.

Seit TypeScript 4.7 akzeptiert infer eine Inline-Einschränkung: infer N extends number filtert die Erfassung, und seit 4.8 wandelt der Compiler ein numerisches String-Literal sogar in ein Zahlenliteral um. Eine weitere Feinheit der Spezifikation: Ein bedingter Typ, dessen Eingabe noch einen ungelösten generischen Parameter enthält, bleibt aufgeschoben; er wird erst an der Instanziierungsstelle ausgewertet, was erklärt, warum manche Fehler erst auf der Seite des Aufrufers auftauchen.

TYPESCRIPT
type ToNumber<S extends string> =
  S extends `${infer N extends number}` ? N : never;

type Port = ToNumber<"8080">; // the literal 8080
type Bad = ToNumber<"abc">;   // never
Einschränkung für infer und Umwandlung in ein Literal (4.8).

Wissenscheck

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

  1. Welchen Typ ergibt Exclude<boolean, true>?
    • boolean
    • false
    • never
    • true
  2. Was ergibt IsNever<never> bei type IsNever<T> = T extends never ? true : false;?
    • true
    • false
    • never
    • boolean
  3. Zwei infer P mit demselben Namen an der Position eines Funktionsparameters ergeben:
    • eine Union der Kandidaten
    • eine Intersection der Kandidaten
    • den ersten angetroffenen Kandidaten
    • einen Kompilierfehler