stackademic

The leading education platform for anyone with an interest in software development.

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 never exhaustiveness case, so new union members slip through
  • Using as casts where a real guard would let the compiler verify the branch
  • Relying on typeof null === "object", which fails to exclude null
  • Mutating a narrowed variable, which widens it back to the declared type

See Also

type-guards unions typescript-conditional-types