Skip to content

Type Narrowing

When working with union types, TypeScript needs to know which specific type you’re using in a given block of code. Type narrowing is the process of refining a variable’s type based on checks like typeof, instanceof, or equality comparisons.

This is similar to guarding in other languages which is a way to restrict control flow based on certain conditions. With TypeScript, these checks act as type guards, allowing the compiler to narrow the type and ensure safe access to properties or methods.

There are several ways to narrow types.

Check for Undefined

A common situation is checking if an optional function argument is undefined using a simple equality check:

ts
function greet(name?: string) {
  if (name === undefined) {
    console.log("Hello, stranger!");
  } else {
    // TypeScript knows name is a string here
    console.log(`Hello, ${name}!`);
  }
}

greet();           // "Hello, stranger!"
greet("Alice");    // "Hello, Alice!"

Use typeof

For union types with different primitive types, you can use typeof to narrow the type:

ts
type StringOrNumber = string | number;

function formatValue(value: StringOrNumber) {
  if (typeof value === "string") {
    // TypeScript knows value is a string here
    console.log(value.toUpperCase());
  } else {
    // TypeScript knows value is a number here
    console.log(value.toFixed(2));
  }
}

// Usage:
formatValue("hello");  // "HELLO"
formatValue(3.14159);  // "3.14"

Use instanceof for Classes

The instanceof operator is another way to narrow types when working with classes. It's a JavaScript operator that checks whether an object was created by a specific class. TypeScript uses it to narrow types by analyzing the object’s prototype chain. It works only with class-based types, not with interfaces or plain objects.

ts
class Circle {
  constructor(public radius: number) {}
}

class Square {
  constructor(public size: number) {}
}

type Shape = Circle | Square;

function getArea(shape: Shape): number {
  if (shape instanceof Circle) {
    // TypeScript knows shape is a Circle here
    return Math.PI * shape.radius ** 2;
  } else {
    // TypeScript knows shape is a Square here
    return shape.size ** 2;
  }
}

Discriminated Union

You can narrow union types by checking the value of a common property. This pattern is often called a discriminated union, where each variant has a shared field (like status) with a distinct literal value:

ts
type LoadingState = { status: "loading" };
type SuccessState = { status: "success"; data: string };
type ErrorState = { status: "error"; message: string };

type AppState = LoadingState | SuccessState | ErrorState;

function handleState(state: AppState) {
  if (state.status === "loading") {
    console.log("Loading...");
  } else if (state.status === "success") {
    // TypeScript knows state has a data property here
    console.log(`Data: ${state.data}`);
  } else {
    // TypeScript knows state has a message property here
    console.log(`Error: ${state.message}`);
  }
}

This approach helps TypeScript safely infer which properties are available in each branch, based on the status value.

Function Overloading

TypeScript allows function overloading by writing multiple function signatures followed by a single implementation.

ts
// Overload signatures - these define what TypeScript knows about the function
function formatValue(value: number): number;  
function formatValue(value: string): string;  

// Implementation signature - this is the actual function body that gets executed
function formatValue(value: any): any {       
  return value;
}

// Usage:
console.log(formatValue("hello")); // "hello"
console.log(formatValue(42));      // 42

But it's more common and often simpler to use union types directly:

ts
// Using a union type instead of overloading:
function formatValue(value: string | number): string {
  return String(value);
}

// Usage:
console.log(formatValue("hello")); // "hello"
console.log(formatValue(42));      // "42"

External Resources