Skip to content

Type Narrowing

Type narrowing is the process by which TypeScript refines a variable’s type based on runtime checks. You start with a wide type—like string | number—and TypeScript narrows it as you inspect it.

This is essential for working with:

  • union types
  • external data
  • DOM events
  • APIs
  • optional fields
  • discriminated unions

Let’s break down the main narrowing tools.


Narrowing with typeof

typeof is the simplest and most common narrowing operator.

Example: Narrowing a union

function printValue(value: string | number) {
  if (typeof value === "string") {
    console.log(value.toUpperCase()) // value: string
  } else {
    console.log(value.toFixed(2))    // value: number
  }
}

TypeScript understands:

  • inside the if, value is a string
  • inside the else, value is a number

Supported typeof checks

You can narrow using:

  • "string"
  • "number"
  • "boolean"
  • "bigint"
  • "symbol"
  • "undefined"
  • "object"
  • "function"

Narrowing with instanceof

Use instanceof when working with classes or built‑in objects.

Example: Date vs string

function format(input: string | Date) {
  if (input instanceof Date) {
    return input.toISOString() // input: Date
  }
  return input.toUpperCase()   // input: string
}

Common use cases

  • Date
  • Error
  • custom classes
  • DOM elements (HTMLElement, HTMLInputElement, etc.)

Narrowing with the in Operator

Use in when narrowing based on object properties.

Example: Two object shapes

type User = { name: string }
type Admin = { name: string; permissions: string[] }

function printInfo(person: User | Admin) {
  if ("permissions" in person) {
    console.log("Admin:", person.permissions)
  } else {
    console.log("User:", person.name)
  }
}

TypeScript narrows based on the presence of a property.


Literal Types

Literal types represent exact values, not just general types.

Example: Literal strings

let direction: "up" | "down" | "left" | "right"

This is far safer than:

let direction: string

Literal numbers

type StatusCode = 200 | 400 | 404 | 500

Literal booleans

type Flag = true | false

Literal types are the foundation of discriminated unions.


Discriminated Unions

Discriminated unions are one of TypeScript’s most powerful features.

They combine:

  • union types
  • literal types
  • narrowing

A discriminated union is a union of objects that all share a discriminant property—a literal field that identifies the variant.

Example: Shape types

type Circle = {
  kind: "circle"
  radius: number
}

type Square = {
  kind: "square"
  size: number
}

type Rectangle = {
  kind: "rectangle"
  width: number
  height: number
}

type Shape = Circle | Square | Rectangle

Using a discriminated union

function area(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2

    case "square":
      return shape.size ** 2

    case "rectangle":
      return shape.width * shape.height

    default:
      const _exhaustive: never = shape
      return _exhaustive
  }
}

Why this is powerful

  • TypeScript narrows based on shape.kind
  • Each case gets full autocomplete
  • Missing cases cause compile‑time errors
  • Impossible states become impossible to represent

This is the pattern behind:

  • Redux reducers
  • API response modeling
  • State machines
  • UI component states
  • Error handling

Putting It All Together

Here’s a realistic example combining all narrowing techniques:

type Result =
  | { status: "success"; data: string }
  | { status: "error"; error: Error }
  | { status: "loading" }

function handle(result: Result) {
  if (result.status === "success") {
    console.log(result.data.toUpperCase())
  } else if (result.status === "error") {
    console.error(result.error.message)
  } else {
    console.log("Loading...")
  }
}

TypeScript ensures:

  • data only exists on "success"
  • error only exists on "error"
  • "loading" has neither
  • no unreachable or invalid states

This is the core of safe, expressive TypeScript.


Summary

In this lesson, you learned how TypeScript refines types at runtime using:

1. Narrowing operators

  • typeof for primitives
  • instanceof for classes
  • in for object shapes

2. Literal types

  • exact string/number/boolean values
  • foundation for precise modeling

3. Discriminated unions

  • the most powerful pattern in TypeScript
  • safe, expressive, and exhaustive
  • ideal for modeling real‑world state

These tools unlock TypeScript’s full potential and prepare you for advanced topics like custom type guards and exhaustive control flow analysis.