TypeScript Narrowing and Control Flow Analysis
Learn how TypeScript narrows types through control flow, using typeof, instanceof, in, and discriminated unions to write safer code.
What you'll learn
- ✓What narrowing is
- ✓typeof and instanceof guards
- ✓The in operator
- ✓Discriminated unions
- ✓Custom type predicates
Prerequisites
- •Comfortable with JS
What and Why
TypeScript narrowing is the process by which the compiler refines a broader type to a more specific one based on runtime checks you write. If a variable is typed string | number and you check typeof x === 'string', inside that branch the compiler treats x as string. This is called control flow analysis: TypeScript tracks branches, assignments, and exits to know what is possible at every line.
Why does this matter? Without narrowing, you would need casts everywhere. With it, your code stays JavaScript-shaped while gaining strong guarantees. Narrowing is the bridge between a flexible type system and ergonomic code.
Mental Model
Think of every variable as carrying a set of possible types. Each control flow construct either shrinks that set or restores it. An if (typeof x === 'string') block subtracts non-string members; the else block keeps the rest. After the block, the set widens again unless the branch exits.
value: string | number | null
|
if (value == null) ----> narrowed: null (return)
|
else if (typeof value === 'string') ----> string
|
else --------------------------------> number Hands-on Example
Consider a function that prints a value of mixed type.
function describe(value: string | number | null): string {
if (value == null) return 'nothing';
if (typeof value === 'string') return value.toUpperCase();
return value.toFixed(2);
}
Each branch removes possibilities. By the final return, only number remains, so toFixed is safe. Now add a discriminated union for richer data:
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number };
function area(s: Shape) {
switch (s.kind) {
case 'circle': return Math.PI * s.radius ** 2;
case 'square': return s.side ** 2;
}
}
The kind field is the discriminant; switching on it narrows s to one variant per case. You can also write custom type predicates when built-in guards do not suffice:
function isUser(x: unknown): x is { id: string; name: string } {
return typeof x === 'object' && x !== null
&& 'id' in x && 'name' in x;
}
Returning x is T teaches the compiler to narrow callers automatically.
Common Pitfalls
A few traps catch most people:
- Re-widening after function calls. If you narrow a property and then call any function, TypeScript may assume the property changed. Pull the value into a local:
const v = obj.value;. - Truthy checks on numbers.
if (n)excludes0as well asundefined, which is rarely what you want. Prefern != null. - Aliasing breaks narrowing. Destructuring before the check, or assigning to another variable, can erase prior refinements.
unknownvsany. Useunknownfor input you have not validated;anysilently disables every check.- Bad predicates. A predicate that returns the wrong shape lies to the compiler and will crash at runtime.
Best Practices
Lean on discriminated unions whenever you model variants; they read clearly and narrow effortlessly. Prefer unknown over any at boundaries (network, storage) and write predicates or use a runtime validator. Keep narrowing close to the value: do not pass a partially narrowed object into another function expecting the narrow type unless you encode the guarantee in its signature.
Use the satisfies operator to verify literal shapes without widening, and use never in exhaustive switches to catch missed variants at compile time:
function assertNever(x: never): never { throw new Error(String(x)); }
Wrap-up
Narrowing is one of TypeScript’s most underrated features. Combine typeof, instanceof, in, discriminated unions, and custom predicates, and most casts disappear. Code reads like plain JavaScript, but the compiler is silently proving correctness for you on every branch.
Related articles
- TypeScript TypeScript Type Guards Deep Dive
Master TypeScript type guards: typeof, instanceof, discriminated unions, user-defined predicates, and assertion functions. Narrow types safely and idiomatically.
- TypeScript TypeScript Discriminated Unions
How to model variant types in TypeScript with discriminated unions: design, narrowing, exhaustiveness checks, and real-world patterns.
- TypeScript TypeScript Branded Types Tutorial
Use branded types in TypeScript to add nominal safety on top of structural types. Stop mixing up UserIds, emails, and raw strings at compile time.
- TypeScript TypeScript Conditional Types Tutorial
Learn conditional types in TypeScript: distribution, infer, and how to build expressive utility types that adapt to the input shape.