TypeScript narrows types based on control flow analysis.

Control Flow Analysis

TypeScript analyzes your code to narrow types.

function example(x: string | number) {
  if (typeof x === "string") {
    // x is string here
    return x.toUpperCase();
  }
  // x is number here
  return x.toFixed(2);
}

Truthiness Narrowing

TypeScript narrows based on truthy/falsy checks.

function process(value: string | null | undefined) {
  if (value) {
    // value is string here (not null or undefined)
    console.log(value.length);
  }
  // value might be null or undefined here
}

Equality Narrowing

TypeScript narrows based on equality checks.

function example(x: string | number, y: string | boolean) {
  if (x === y) {
    // Both are string here
    return x.toUpperCase() + y.toUpperCase();
  }
}

Discriminated Union Narrowing

TypeScript narrows based on discriminant properties.

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      // shape is { kind: "circle"; radius: number }
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      // shape is { kind: "rectangle"; width: number; height: number }
      return shape.width * shape.height;
  }
}

in Operator Narrowing

TypeScript narrows based on property existence.

interface Dog {
  bark(): void;
}

interface Cat {
  meow(): void;
}

function makeSound(pet: Dog | Cat) {
  if ("bark" in pet) {
    pet.bark(); // pet is Dog
  } else {
    pet.meow(); // pet is Cat
  }
}

instanceof Narrowing

TypeScript narrows based on instanceof checks.

class Animal {
  move(): void {}
}

class Dog extends Animal {
  bark(): void {}
}

function process(animal: Animal) {
  if (animal instanceof Dog) {
    animal.bark(); // animal is Dog
  }
}

Type Predicates

User-defined functions that narrow types.

function isString(value: unknown): value is string {
  return typeof value === "string";
}

function process(value: unknown) {
  if (isString(value)) {
    value.toUpperCase(); // value is string
  }
}

Assertion Functions

Functions that assert and narrow types.

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

function process(value: unknown) {
  assertIsNumber(value);
  value.toFixed(2); // value is number
}

Exercises

Exercise 1: Control Flow

// Create a function that:
// - Takes string | number | boolean
// - Uses if/else to narrow the type
// - Handles each case appropriately

Exercise 2: Discriminated Unions

// Create a discriminated union for events
// Use switch to narrow and handle each event type

Exercise 3: Truthiness

// Create a function that:
// - Takes string | null | undefined
// - Uses truthiness to narrow
// - Returns a non-null string

Exercise 4: Type Predicates

// Create type predicates for:
// - isArray
// - isObject
// - isNumber
// Use them to narrow types

Exercise 5: Complex Narrowing

// Create a function with multiple narrowing steps
// Combine typeof, instanceof, and type predicates

Best Practices

  1. Let TypeScript infer narrowing - It's very good at it
  2. Use discriminated unions - Makes narrowing explicit
  3. Create reusable type guards - For common patterns
  4. Handle all cases - Use exhaustive checks
  5. Trust the compiler - TypeScript's narrowing is sound

Common Mistakes

  1. Not trusting narrowing
// ❌ Unnecessary assertion
function process(value: string | number) {
  if (typeof value === "string") {
    const str = value as string; // Unnecessary!
  }
}

// ✅ Trust narrowing
function process(value: string | number) {
  if (typeof value === "string") {
    // value is already string here
  }
}
  1. Breaking narrowing with reassignment
// ❌ Reassignment breaks narrowing
let value: string | number = getValue();
if (typeof value === "string") {
  value = 42; // Breaks narrowing
  value.toUpperCase(); // Error!
}
  1. Not handling all union members
// ❌ Missing case
function process(value: "a" | "b" | "c") {
  if (value === "a") {
    // Handle a
  }
  // Missing b and c
}

// ✅ Handle all cases
function process(value: "a" | "b" | "c") {
  switch (value) {
    case "a":
      // Handle a
      break;
    case "b":
      // Handle b
      break;
    case "c":
      // Handle c
      break;
  }
}

Next Steps

Learn about Mapped Types.