Narrowing and Control-Flow Analysis
Narrowing is how TypeScript refines a union to the members that can reach a point in your code. It reads ordinary JavaScript checks, so knowing which checks it understands, and where it stops trusting them, is the core skill.
Key points
- 1
Built-in guards:
typeof,instanceof,in,===/!==(including against literals), truthiness andArray.isArray. Each narrows both branches, and early returns narrow the rest of the function. - 2
A discriminated union has a literal-typed tag on every member. Comparing the tag (in
if,switchorswitch (true)since 5.3) narrows the whole object;constdestructuring keeps that link (4.6). - 3
Exhaustiveness: in
default, assign the value toneveror writevalue satisfies never, so adding a union member becomes a compile error. - 4
User-defined guards (
x is T) and assertion functions (asserts x is T) are trusted, not verified. Since 5.5 many simple arrow guards are inferred, but only when true means T and false means not-T. - 5
Narrowing is kept in closures for
const, and since 5.4 forletand parameters with no assignment after the closure is created. Hoisted function declarations do not keep it. - 6
Property narrowing (
if (r.data !== null)) survives function calls even though a call might mutate the object. That is a deliberate, unsound trade-off. - 7
Values of a type parameter narrow by intersection (
T & string), andinonobjectaddsRecord<"key", unknown>(4.9).
Common traps
if (value)also rejects0,""andNaN. Compare withundefinedornullexplicitly when those are valid values.Array.isArrayonstring | readonly string[]givesany[]in the true branch and does not narrow the else branch.A predicate such as
n is numberthat also checksn > 0sends negative numbers into the branch typed asstring.
Read the source
Test yourself on Narrowing and Control-Flow Analysis
Ten questions, with the answer and explanation after each one.