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
| Form | Beispiel | Engt ein |
|---|---|---|
| typeof | typeof x === "string" | primitive Typen und Funktionen |
| Truthiness | if (x) | entfernt null, undefined und falsy Literale |
| Gleichheit | x === "a", x == null, x !== undefined | Literale, null, undefined |
in | "swim" in pet | Unions aus Objekten, anhand einer Eigenschaft |
| instanceof | err instanceof TypeError | Klasseninstanzen |
Array.isArray | Array.isArray(x) | Arrays gegenüber allem anderen |
| Zuweisung | x = 5 | auf den zugewiesenen Typ |
| Type Predicate | function isUser(x: unknown): x is User | alles, was du prüfen kannst |
| Assertion-Funktion | function assertUser(x: unknown): asserts x is User | alles nach dem Aufruf |
| Diskriminanten-Eigenschaft | switch (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.nameengtobj.name(undobj["name"]) ein, aber nichtobj[key], wennkeyeinestring-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
letihr Narrowing nur, wenn sie nach dem Erzeugen des Callbacks nicht erneut zugewiesen wird. Eineconstoder ein Parameter, der nie neu zugewiesen wird, bleibt eingeengt. - Prüfungen in Hilfsfunktionen. Eine Funktion
isString(x: unknown): booleansagt dem Compiler nichts. Gib ihr einen Type Predicate als Rückgabetyp,x is string, dann engen Aufrufe davon ein wietypeof.
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).