Type Narrowing and Type Guards

Introduction

Union types describe possible shapes; narrowing refines them inside branches so TypeScript knows which members and methods are safe. Built-in checks (typeof, instanceof, in) and custom type predicates (value is Type) are how you turn unions into precise, checked code.

Prerequisites

Narrowing with typeof

For primitive unions:

typescript
function padLeft(value: string, padding: string | number): string {
  if (typeof padding === "number") {
    return " ".repeat(padding) + value;
  }
  return padding + value;
}

typeof works well for: string, number, boolean, bigint, symbol, undefined, function. It does not distinguish object classes beyond object / function.

Narrowing with instanceof

For class instances:

Code explanation:

  • instanceof checks the prototype chain at runtime
  • Prefer discriminated unions for plain objects (no class)

Narrowing with in

Check property existence on objects:

typescript
type Fish = { swim(): void };
type Bird = { fly(): void };
 
function move(animal: Fish | Bird): void {
  if ("swim" in animal) {
    animal.swim();
  } else {
    animal.fly();
  }
}

Useful when types differ by member names.

Equality and Literal Narrowing

typescript
type Status = "pending" | "done" | "failed";
 
function icon(status: Status): string {
  if (status === "pending") {
    return "⏳";
  }
  if (status === "done") {
    return "✅";
  }
  return "❌";
}

After checking all literals, status in the final branch is "failed".

Truthiness Narrowing

typescript
function printName(name: string | null | undefined): void {
  if (!name) {
    console.log("Guest");
    return;
  }
  console.log(name.toUpperCase());
}

Code explanation:

  • Early return removes null and undefined from the rest of the block
  • Empty string "" is also falsy—decide if that is intended

Warning

0 and "" Are Falsy

if (value) removes 0 and "" too. Use value != null or explicit checks when those are valid.

Discriminated Unions

Shared field with literal values:

Code explanation:

  • kind discriminates; each case narrows shape
  • never assignment catches missing cases when Shape grows

User-Defined Type Guards (Type Predicates)

Code explanation:

  • Return type account is Admin tells TypeScript the branch type
  • Implementation must be honest—wrong predicates cause runtime bugs

Predicate on primitives

typescript
function isString(value: unknown): value is string {
  return typeof value === "string";
}
 
function len(value: unknown): number {
  if (isString(value)) {
    return value.length;
  }
  return 0;
}

Assertion Functions (Brief)

typescript
function assertIsNumber(value: unknown): asserts value is number {
  if (typeof value !== "number") {
    throw new Error("Expected number");
  }
}
 
function double(value: unknown): number {
  assertIsNumber(value);
  return value * 2;
}

After assertIsNumber, value is number in that scope.

Control Flow Analysis

TypeScript tracks assignments across branches:

typescript
function example(x: string | number) {
  if (typeof x === "string") {
    console.log(x.length);
  } else {
    console.log(x.toFixed(2));
  }
}

Reassignments can widen types again—keep functions small when unions are complex.

Practical Example: Parsing API Payload

Common Mistakes

MistakeResultFix
No discriminant on object unionsProperties not narrowedAdd kind / type field
Fake type predicateRuntime errorsMatch predicate to real check
Relying on typeof for arraysobject, not arrayUse Array.isArray
Mutating narrowed variableType widens againUse const or separate variables

FAQ

Narrowing vs type assertion?

Narrowing is checked by control flow. Assertions override the compiler—prefer narrowing when possible.

Does narrowing work in callbacks?

Sometimes not—let mutated in closures can confuse analysis. Pass narrowed values into callbacks as parameters.

Can I narrow Promise types?

Await the promise, then narrow the resolved value.

instanceof with interfaces?

Interfaces do not exist at runtime—use discriminated fields or custom guards.

What is control flow narrowing?

The compiler’s analysis of if, switch, return, and assignments to refine types.

Are type guards slow?

Small in / typeof checks are cheap. Prefer clarity over micro-optimization.