Type narrowing is TypeScript working out a more specific type for a value at a particular point in the code, based on the checks the code has already made. A string | number parameter becomes string inside if (typeof x === "string") and number in the else.
Calling value.toFixed(2) before the check would be a compile error, because toFixed does not exist on string. The check is ordinary JavaScript and runs at run time; the narrowing is the compiler reading it and adjusting the type. Nothing extra is emitted.
Control Flow Analysis
TypeScript follows every path through a function: if/else, early return and throw, switch, loops, and the short-circuit operators &&, ||, ?? and ?:. At each point, a variable's type is whatever is still possible there.
The early-return style ("guard clauses") is the most readable way to narrow: handle the odd cases first and the rest of the function works with the clean type.
Every Way to Narrow
| Form | Example | Narrows |
|---|---|---|
| typeof | typeof x === "string" | primitives and functions |
| Truthiness | if (x) | removes null, undefined and falsy literals |
| Equality | x === "a", x == null, x !== undefined | literals, null, undefined |
in | "swim" in pet | object unions, by property |
| instanceof | err instanceof TypeError | class instances |
Array.isArray | Array.isArray(x) | arrays vs everything else |
| Assignment | x = 5 | to the assigned type |
| Type predicate | function isUser(x: unknown): x is User | anything you can check |
| Assertion function | function assertUser(x: unknown): asserts x is User | everything after the call |
| Discriminant property | switch (shape.kind) | tagged unions |
The last three are covered on the type guards and discriminated unions pages. The rest are below.
Truthiness Narrowing
if (x) removes null and undefined (and false, 0, "" literal types). It is short, and it has one classic trap: 0 and "" are falsy, so valid values get treated as missing.
For numbers and strings, compare with undefined or null explicitly (or use ??). Truthiness is fine for objects, arrays and functions, which are never falsy.
Equality Narrowing
===, !==, == and != narrow both sides. Comparing with a literal narrows to that literal; == null (loose equality) matches both null and undefined in one check, and is the one place loose equality is idiomatic.
Comparing two variables narrows both to what they could have in common: if a: string | number and b: string | boolean pass a === b, both are string inside the if.
The in Operator
"key" in obj narrows a union of object types to the members that have (or may have) that property.
in also works on unknown once you know it is an object: after typeof v === "object" && v !== null && "id" in v, TypeScript knows v has an id property of type unknown. For unions you design yourself, a shared tag property (kind: "fish") is clearer than probing for methods: that pattern is called a discriminated union.
Assignment Narrowing
A variable has a declared type, and a narrowed type that follows its assignments. Assigning a value narrows it to that value's type, up to the declared type.
Where Narrowing Is Lost
Narrowing is local and conservative. A few situations reset it:
- A different expression. Checking
obj.namenarrowsobj.name(andobj["name"]), but notobj[key]whenkeyis astringvariable rather than a literal, and not a copy made before the check. - Callbacks and reassignment. Inside a callback, a narrowed
letkeeps its narrowing only if it is not assigned again after the callback is created. Aconstor a parameter that is never reassigned stays narrowed. - Checks hidden in helpers. A function
isString(x: unknown): booleantells the compiler nothing. Give it a type predicate return type,x is string, and calls to it narrow liketypeofdoes.
The compiler reports index.ts(5,38): error TS18048: 'x' is possibly 'undefined'. The callback could run later, after x = undefined. Delete that last assignment (or copy the value into a const inside the if) and it compiles and prints 5 twice.
A helper that returns boolean can be fixed by declaring what it proves. That is what a type guard is:
function isString(value: unknown): value is string {
return typeof value === "string";
}
Since TypeScript 5.5 the compiler infers such predicates for simple arrow functions, which is why list.filter((x) => x !== undefined) now returns an array without undefined.
Frequently Asked Questions
What is type narrowing in TypeScript?
Narrowing is TypeScript refining a variable's type inside a block based on a check the code performs. After if (typeof x === "string"), a string | number is just string inside the if and just number in the else. The compiler follows if, else, return, switch, &&, || and ?: to work out the type at each point, which is called control flow analysis.
Why is TypeScript not narrowing my type?
Common causes: the check is on a different expression than the one you use (obj.a checked, obj[key] used with a key of type string); the value is a let that is reassigned after a callback was created, so the callback loses the narrowing; or the check is hidden in a helper that returns plain boolean instead of a type predicate x is T.
Does type narrowing work at runtime?
The checks do: typeof, instanceof, in and === are ordinary JavaScript that runs. The narrowing itself is compile-time only. TypeScript reads your runtime checks and adjusts the static types to match them, and nothing is added to the emitted JavaScript.
How do I narrow an unknown type in TypeScript?
With the same checks: typeof value === "string", Array.isArray(value), value instanceof Date, or for objects typeof value === "object" && value !== null && "id" in value. For reusable checks write a type guard function with a value is T return type.
How do I filter undefined out of an array in TypeScript?
items.filter((x) => x !== undefined) returns T[] without undefined since TypeScript 5.5, which infers the callback as a type predicate. On older versions write the predicate yourself: items.filter((x): x is T => x !== undefined).