Menu

TypeScript Type Narrowing: so funktioniert Control Flow Analysis

Narrowing ist die Art, wie TypeScript einen weiten Typ wie string | number in einem if, einem switch oder nach einem frühen return zu einem konkreten Typ macht. Alle Formen des Narrowing an einem Ort: typeof, Truthiness, Gleichheit, in, instanceof, Zuweisungen, Type Predicates und Discriminated Unions.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Type Narrowing bedeutet, dass TypeScript für einen Wert an einer bestimmten Stelle im Code einen spezifischeren Typ ermittelt, anhand der Prüfungen, die der Code bereits gemacht hat. Ein Parameter vom Typ string | number wird in if (typeof x === "string") zu string und im else zu number.

value.toFixed(2) vor der Prüfung aufzurufen wäre ein Compilerfehler, weil toFixed auf string nicht existiert. Die Prüfung ist gewöhnliches JavaScript und läuft zur Laufzeit; das Narrowing ist der Compiler, der sie liest und den Typ anpasst. Es wird nichts zusätzlich erzeugt.

Control Flow Analysis

TypeScript folgt jedem Pfad durch eine Funktion: if/else, frühes return und throw, switch, Schleifen und die Kurzschlussoperatoren &&, ||, ?? und ?:. An jeder Stelle ist der Typ einer Variablen das, was dort noch möglich ist.

Der Stil mit frühem return („Guard Clauses“) ist die lesbarste Art einzuengen: Behandle die Sonderfälle zuerst, und der Rest der Funktion arbeitet mit dem sauberen Typ.

Alle Arten des Narrowing

FormBeispielEngt ein
typeoftypeof x === "string"primitive Typen und Funktionen
Truthinessif (x)entfernt null, undefined und falsy Literale
Gleichheitx === "a", x == null, x !== undefinedLiterale, null, undefined
in"swim" in petUnions aus Objekten, anhand einer Eigenschaft
instanceoferr instanceof TypeErrorKlasseninstanzen
Array.isArrayArray.isArray(x)Arrays gegenüber allem anderen
Zuweisungx = 5auf den zugewiesenen Typ
Type Predicatefunction isUser(x: unknown): x is Useralles, was du prüfen kannst
Assertion-Funktionfunction assertUser(x: unknown): asserts x is Useralles nach dem Aufruf
Diskriminanten-Eigenschaftswitch (shape.kind)Unions mit Tag

Die letzten drei werden auf den Seiten zu Type Guards und Discriminated Unions behandelt. Die übrigen folgen hier.

Narrowing über Truthiness

if (x) entfernt null und undefined (sowie die Literaltypen false, 0 und ""). Das ist kurz und hat eine klassische Falle: 0 und "" sind falsy, gültige Werte werden also als fehlend behandelt.

Bei Zahlen und Strings vergleichst du ausdrücklich mit undefined oder null (oder nimmst ??). Truthiness passt für Objekte, Arrays und Funktionen, die nie falsy sind.

Narrowing über Gleichheit

===, !==, == und != engen beide Seiten ein. Der Vergleich mit einem Literal engt auf dieses Literal ein; == null (lose Gleichheit) trifft in einer Prüfung sowohl null als auch undefined und ist die eine Stelle, an der lose Gleichheit üblich ist.

Der Vergleich zweier Variablen engt beide auf das ein, was sie gemeinsam haben könnten: Wenn a: string | number und b: string | boolean den Test a === b bestehen, sind beide im if string.

Der Operator in

"key" in obj engt eine Union aus Objekttypen auf die Member ein, die diese Eigenschaft haben (oder haben könnten).

in funktioniert auch bei unknown, sobald du weißt, dass es ein Objekt ist: Nach typeof v === "object" && v !== null && "id" in v weiß TypeScript, dass v eine Eigenschaft id vom Typ unknown hat. Bei Unions, die du selbst entwirfst, ist eine gemeinsame Tag-Eigenschaft (kind: "fish") klarer, als nach Methoden zu tasten: Dieses Muster heißt Discriminated Union.

Narrowing durch Zuweisung

Eine Variable hat einen deklarierten Typ und einen eingeengten Typ, der ihren Zuweisungen folgt. Einen Wert zuzuweisen engt sie auf den Typ dieses Werts ein, bis hin zum deklarierten Typ.

Wo Narrowing verloren geht

Narrowing ist lokal und vorsichtig. Einige Situationen setzen es zurück:

  • Ein anderer Ausdruck. Eine Prüfung von obj.name engt obj.name (und obj["name"]) ein, aber nicht obj[key], wenn key eine string-Variable statt eines Literals ist, und auch keine Kopie, die vor der Prüfung angelegt wurde.
  • Callbacks und Neuzuweisung. In einem Callback behält eine eingeengte let ihr Narrowing nur, wenn sie nach dem Erzeugen des Callbacks nicht erneut zugewiesen wird. Eine const oder ein Parameter, der nie neu zugewiesen wird, bleibt eingeengt.
  • Prüfungen in Hilfsfunktionen. Eine Funktion isString(x: unknown): boolean sagt dem Compiler nichts. Gib ihr einen Type Predicate als Rückgabetyp, x is string, dann engen Aufrufe davon ein wie typeof.

Der Compiler meldet index.ts(5,38): error TS18048: 'x' is possibly 'undefined'. Der Callback könnte später laufen, nach x = undefined. Lösche diese letzte Zuweisung (oder kopiere den Wert im if in eine const), dann kompiliert es und gibt zweimal 5 aus.

Eine Hilfsfunktion, die boolean zurückgibt, lässt sich reparieren, indem sie deklariert, was sie beweist. Genau das ist ein Type Guard:

function isString(value: unknown): value is string {
  return typeof value === "string";
}

Seit TypeScript 5.5 leitet der Compiler solche Predicates für einfache Arrow Functions ab, deshalb gibt list.filter((x) => x !== undefined) jetzt ein Array ohne undefined zurück.

Häufig gestellte Fragen

Was ist Type Narrowing in TypeScript?

Narrowing bedeutet, dass TypeScript den Typ einer Variablen in einem Block anhand einer Prüfung im Code verfeinert. Nach if (typeof x === "string") ist ein string | number im if nur noch string und im else nur noch number. Der Compiler folgt if, else, return, switch, &&, || und ?:, um den Typ an jeder Stelle zu ermitteln; das nennt man Control Flow Analysis.

Warum engt TypeScript meinen Typ nicht ein?

Häufige Ursachen: Die Prüfung betrifft einen anderen Ausdruck als den, den du verwendest (obj.a geprüft, obj[key] mit einem key vom Typ string verwendet); der Wert ist eine let, die neu zugewiesen wird, nachdem ein Callback erzeugt wurde, sodass der Callback das Narrowing verliert; oder die Prüfung steckt in einer Hilfsfunktion, die ein einfaches boolean statt eines Type Predicates x is T zurückgibt.

Funktioniert Type Narrowing zur Laufzeit?

Die Prüfungen schon: typeof, instanceof, in und === sind gewöhnliches JavaScript, das ausgeführt wird. Das Narrowing selbst passiert nur beim Kompilieren. TypeScript liest deine Laufzeitprüfungen und passt die statischen Typen daran an, und dem erzeugten JavaScript wird nichts hinzugefügt.

Wie engt man in TypeScript einen Typ unknown ein?

Mit denselben Prüfungen: typeof value === "string", Array.isArray(value), value instanceof Date oder für Objekte typeof value === "object" && value !== null && "id" in value. Für wiederverwendbare Prüfungen schreibst du eine Type-Guard-Funktion mit Rückgabetyp value is T.

Wie filtere ich in TypeScript undefined aus einem Array?

items.filter((x) => x !== undefined) gibt seit TypeScript 5.5 T[] ohne undefined zurück, weil der Callback als Type Predicate abgeleitet wird. In älteren Versionen schreibst du das Predicate selbst: items.filter((x): x is T => x !== undefined).

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S