Type Narrowing
How control flow analysis refines union types down to a single member
Overview
Narrowing is how TypeScript refines a broad type (often a union) to something more specific based on runtime checks. The compiler performs control flow analysis, tracking how conditionals, assignments, and returns constrain a variable within each branch. Mastering narrowing removes the need for unsafe casts.
Syntax / Usage
Common narrowing tools include typeof, truthiness checks, equality, the in operator, and discriminated unions with a shared literal tag.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
function area(shape: Shape): number {
// Discriminant narrowing on the "kind" tag
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2
case "square":
return shape.side ** 2
default: {
// Exhaustiveness check: errors if a variant is unhandled
const _exhaustive: never = shape
return _exhaustive
}
}
}
Examples
Truthiness narrowing removes null and undefined from a union.
function greet(name: string | null) {
if (!name) return "Hello, guest"
return `Hello, ${name.toUpperCase()}` // name is string here
}
Assignment narrows the declared type to the assigned literal.
let value: string | number
value = "ready"
value.toUpperCase() // ok: narrowed to string after assignment
The in operator distinguishes shapes without a discriminant field.
type Result = { data: string } | { error: string }
function handle(result: Result) {
if ("data" in result) return result.data
return `Failed: ${result.error}`
}
Common Mistakes
- Narrowing that is lost inside callbacks—re-check the value in the closure
- Forgetting the
neverexhaustiveness case, so new union members slip through - Using
ascasts where a real guard would let the compiler verify the branch - Relying on
typeof null === "object", which fails to excludenull - Mutating a narrowed variable, which widens it back to the declared type
See Also
type-guards unions typescript-conditional-types