Atlas

Roadmap

Foundations

Type Narrowing

Mar 25, 2026

What is Type Narrowing?

Type narrowing is when TypeScript refines a broad type into a more specific one based on a runtime check. Inside the check, TypeScript knows the type is more specific and unlocks the methods and properties that come with it.

function printLength(value: string | number) {
  if (typeof value === "string") {
    console.log(value.length)  // TypeScript knows value is string here
  } else {
    console.log(value.toFixed(2))  // TypeScript knows value is number here
  }
}
typescript

Without the typeof check, TypeScript would not allow you to call .length because number does not have that property.

typeof Narrowing

typeof works well for primitive types: string, number, boolean, undefined.

function format(value: string | number): string {
  if (typeof value === "string") {
    return value.toUpperCase()
  }
  return value.toFixed(2)
}

format("hello")  // "HELLO"
format(3.14159)  // "3.14"
typescript

Truthiness Narrowing

Checking if a value is truthy also narrows its type by removing null and undefined.

function greet(name: string | null) {
  if (name) {
    console.log("Hello, " + name.toUpperCase())  // name is string here
  } else {
    console.log("Hello, stranger")
  }
}
typescript

instanceof Narrowing

instanceof narrows to a class type.

class Dog {
  bark() { console.log("Woof!") }
}

class Cat {
  meow() { console.log("Meow!") }
}

function makeSound(animal: Dog | Cat) {
  if (animal instanceof Dog) {
    animal.bark()   // TypeScript knows it's a Dog
  } else {
    animal.meow()   // TypeScript knows it's a Cat
  }
}
typescript

in Narrowing

The in operator checks if a property exists on an object, which narrows between object types.

type Admin = { role: "admin"; permissions: string[] }
type User = { role: "user"; email: string }

function describe(person: Admin | User) {
  if ("permissions" in person) {
    console.log("Admin with permissions:", person.permissions)
  } else {
    console.log("User email:", person.email)
  }
}
typescript

Discriminated Unions

A discriminated union is a pattern where each type in a union has a shared literal field (called a discriminant) that uniquely identifies it. TypeScript can narrow the union by checking that field.

type Circle = { shape: "circle"; radius: number }
type Square = { shape: "square"; side: number }
type Shape = Circle | Square

function getArea(shape: Shape): number {
  switch (shape.shape) {
    case "circle":
      return Math.PI * shape.radius ** 2   // shape is Circle here
    case "square":
      return shape.side ** 2               // shape is Square here
  }
}
typescript

The shape field is the discriminant. This pattern is very common in TypeScript because it is explicit and exhaustive — if you add a new type to the union, TypeScript will warn you about unhandled cases.

Key Questions

Q: What is type narrowing in TypeScript?

Type narrowing is the process of refining a variable's type from a broad type to a more specific one based on a runtime check. TypeScript tracks these checks and adjusts what it knows about the type inside the branch. Common narrowing techniques include typeof, instanceof, in, and truthiness checks.

Q: What is a discriminated union and why is it useful?

A discriminated union is a union type where each member has a shared literal field with a unique value, called the discriminant. By checking the discriminant, TypeScript can narrow the union to a specific type. It is useful because it makes branching explicit and exhaustive — TypeScript can warn you if you forget to handle a case when you add a new member to the union.

Q: What is the difference between typeof and instanceof for type narrowing?

typeof is used for primitive types: it returns a string like "string", "number", "boolean", or "undefined". instanceof is used for class instances: it checks whether an object was created with a specific constructor. Use typeof for primitives, instanceof for objects created from classes.