Siddhant DevalAuthor
Senior Full-Stack Engineer·Aug 27, 2026·11 min read
Narrowing & Control Flow Analysis
TypeScript tracks type information through every branch of your code. Control Flow Analysis is the engine under narrowing — understanding it eliminates every `as` cast in application code and makes discriminated unions the most powerful pattern in the language.
Technical Series
TypeScript Mastery
Part 2 of 11
Narrowing & Control Flow Analysis
Types are a specification language — not an annotation layer. That discipline shows up most concretely in narrowing: TypeScript does not assign a single type to a variable and call it done. It tracks the precise type at each node in the control flow graph. Every
if, switch, while, and early return updates that tracked type. Understanding this engine — Control Flow Analysis — is what lets you eliminate every as cast from application code.1. Control Flow Analysis — The Engine Under Narrowing
TypeScript's compiler builds a Control Flow Graph (CFG) for every function body. Each node in the graph carries the type of every variable as it is known at that point in the execution path.
1.1 A Minimal Example
typescript
TypeScript sees the
value === null check and narrows the type inside the branch to null. After the branch (which returns), the remaining path can only reach code where value is string. No explicit cast required.
Expand
2. Built-in Narrowing Operators
2.1 typeof Narrowing
typeof returns one of eight string literals: "string", "number", "bigint", "boolean", "symbol", "undefined", "object", "function". TypeScript maps each return value to the correct type.typescript
Performance / Safety Warning
typeof null === "object" — this is a 26-year-old JavaScript spec quirk that TypeScript inherits. Always check value === null explicitly before checking typeof value === "object" if your union includes null.2.2 instanceof Narrowing
instanceof checks the prototype chain and narrows to the constructor's instance type:typescript
2.3 in Narrowing — Discriminating Object Shapes
The
in operator checks whether a property exists on an object. TypeScript uses it to narrow union members by their property presence:typescript
2.4 Truthiness Narrowing
TypeScript narrows to eliminate
null, undefined, 0, "", and false from a union when you use a value in a boolean context:typescript
Performance / Safety Warning
Truthiness narrowing eliminates
"" (empty string) from string, which is often not what you intend. If an empty string is a valid value, check !== null && !== undefined explicitly rather than using a truthiness check.3. Discriminated Unions — The Canonical Narrowing Target
A discriminated union is a union of object types that all share a common literal-typed field (the discriminant). TypeScript's CFA uses the discriminant field to perform exhaustive narrowing.
3.1 Building a Discriminated Union
typescript

Expand
4. User-Defined Type Predicates
When built-in narrowing is insufficient, you can write a type predicate function to teach TypeScript your custom narrowing logic:
typescript
Performance / Safety Warning
TypeScript trusts your type predicate unconditionally. If
isUser contains a bug and returns true for a non-User, the compiler will not catch it — you have taken responsibility for the narrowing logic. This is why type predicates should be minimal and independently tested.5. Assertion Functions
Assertion functions narrow the type for all code after the assertion call — not just inside a branch. They are the TypeScript 3.7+ pattern for imperative guard clauses:
typescript
6. Exhaustiveness Checking with never
When you add a new member to a discriminated union, you want the compiler to find every
switch statement that fails to handle it. The never exhaustiveness pattern achieves this:typescript
Mental Model Check
After all known cases are handled, the
default branch should be unreachable — status should be never. Assigning it to a never-typed variable makes that expectation explicit. When a new union member breaks the invariant, the assignment fails at compile time, not runtime.
Expand
Summary
| Concept | Rule |
|---|---|
| CFA | TypeScript tracks the precise type at each control flow node |
typeof | Narrows to 8 possible primitive types; beware typeof null === "object" |
instanceof | Narrows via prototype chain; requires a constructor reference |
in | Narrows by property presence; the correct tool for duck-typing shapes |
| Truthiness | Eliminates falsy values including "" — only use when empty string is invalid |
| Discriminated union | Common literal kind field gives CFA the clearest possible signal |
| Type predicate | x is T — teaches CFA your custom narrowing; you own the correctness |
| Assertion function | asserts cond — narrows all code following the call, not just one branch |
never exhaustiveness | const _e: never = x in default — compile-time union completeness guard |
What's Next
In Part 3, we cover Generics & Constraints — the type-level equivalent of function parameters. Mastering them eliminates the category ofanyusage that exists solely because a developer didn't know how to preserve type information through a function call.
Research & Synthesis Note
This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.
#TypeScript#Narrowing#Type Guards#Control Flow#Discriminated Unions