Menu

TypeScript Type Narrowing: How Control Flow Analysis Works

Narrowing is how TypeScript turns a wide type like string | number into a specific one inside an if, a switch or after an early return. Every narrowing form in one place: typeof, truthiness, equality, in, instanceof, assignments, type predicates and discriminated unions.

This page includes runnable editors - edit, run, and see output instantly.

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

FormExampleNarrows
typeoftypeof x === "string"primitives and functions
Truthinessif (x)removes null, undefined and falsy literals
Equalityx === "a", x == null, x !== undefinedliterals, null, undefined
in"swim" in petobject unions, by property
instanceoferr instanceof TypeErrorclass instances
Array.isArrayArray.isArray(x)arrays vs everything else
Assignmentx = 5to the assigned type
Type predicatefunction isUser(x: unknown): x is Useranything you can check
Assertion functionfunction assertUser(x: unknown): asserts x is Usereverything after the call
Discriminant propertyswitch (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.name narrows obj.name (and obj["name"]), but not obj[key] when key is a string variable rather than a literal, and not a copy made before the check.
  • Callbacks and reassignment. Inside a callback, a narrowed let keeps its narrowing only if it is not assigned again after the callback is created. A const or a parameter that is never reassigned stays narrowed.
  • Checks hidden in helpers. A function isString(x: unknown): boolean tells the compiler nothing. Give it a type predicate return type, x is string, and calls to it narrow like typeof does.

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

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED