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) removes null and undefined.
  • 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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · ts
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.

Example · ts
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.

Example · ts
function greet(name: string | null): string {
  if (name) {
    return `Hello, ${name.toUpperCase()}!`; // name is string here
  }
  return 'Hello, stranger!';
}

Discussion

  • Be the first to comment on this lesson.
Control-Flow Narrowing — TypeScript | SoundsCode