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
      }
    }

  2. Breaking narrowing with reassignment

    // ❌ Reassignment breaks narrowing
    let value: string | number = getValue();
    if (typeof value === "string") {
      value = 42; // Breaks narrowing
      value.toUpperCase(); // Error!
    }

  3. 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.