Control-Flow Narrowing
TypeScript tracks the flow of your code and quietly refines a value's type at every branch.
Here is one of the loveliest things about TypeScript: it reads your code the way you do. As execution flows through if, &&, return, and throw, the compiler keeps refining what it knows about a variable. This is called control-flow analysis, and narrowing is its output.
The everyday tools
typeof x === "string"— narrows primitives.- A plain truthiness check —
if (user)removesnullandundefined. - Equality against a literal —
if (status === "done").
Inside a truthiness guard, TypeScript strips the falsy parts of the type for you:
function greet(name: string | null) {
if (name) {
// here name is just: string
return name.toUpperCase();
}
return "Anonymous";
}Notice you did not assert anything. You wrote an ordinary guard and the type followed. When you fight the compiler with as, it is often a sign a narrowing check would have been cleaner.
Example
When to use it
- A payment handler uses early returns to narrow a union so the final branch only sees the successful payment type.
- A validation chain narrows unknown input through consecutive if checks until the variable is fully typed.
- A state machine function uses control-flow narrowing to guarantee each branch works with the correct state shape.
More examples
Narrowing via early return
An early return on null/undefined lets TypeScript narrow value to string for all remaining code in the function.
function process(value: string | null | undefined): string {
if (value == null) return 'empty'; // narrows to string below
return value.toUpperCase(); // safe: null/undefined excluded
}Sequential narrowing with if/else
TypeScript narrows the discriminated union in each branch so only the correct variant's properties are accessible.
type Event = { kind: 'click'; x: number; y: number }
| { kind: 'keydown'; key: string };
function handle(e: Event): string {
if (e.kind === 'click') {
return `click at ${e.x},${e.y}`; // e is Click here
} else {
return `key: ${e.key}`; // e is KeyDown here
}
}Narrowing with truthiness
A truthiness check narrows name from string | null to string inside the if block, allowing safe method calls.
function greet(name: string | null): string {
if (name) {
return `Hello, ${name.toUpperCase()}!`; // name is string here
}
return 'Hello, stranger!';
}
Discussion