Narrowing uncertain values

Type narrowing and type guards in TypeScript

Type narrowing lets TypeScript refine union types as you check them. Learn typeof, truthiness, in, instanceof, discriminated unions, and is guards.

Type narrowing is how TypeScript refines a union type as you check it. You start with a value that could be one of several types. After a check, TypeScript knows the exact type in that branch. We saw each technique on its own in the previous lessons. This lesson puts them side by side so you can pick the right one quickly.

This builds on union types, where a value can be one type or another.

typeof

For primitives, typeof is the simplest guard:

function double(value: string | number) {
  if (typeof value === 'string') {
    return value.toUpperCase()
  }
  return value * 2
}

Inside the if block, value is a string. After it, only number is left, so the multiplication compiles.

Truthiness

Checking for null or undefined narrows optional values:

function greet(name: string | undefined) {
  if (!name) {
    return 'Hello stranger'
  }
  return `Hello ${name}`
}

After the check, name is a string. Remember that !name also catches the empty string. Here that is what we want: an empty name is still a stranger.

The in operator

For objects, in checks if a property exists:

type Dog = { bark: () => void }
type Cat = { meow: () => void }

function speak(pet: Dog | Cat) {
  if ('bark' in pet) {
    pet.bark()
  } else {
    pet.meow()
  }
}

Only Dog has bark, so the if branch is a Dog and the else branch is a Cat.

instanceof

For class instances, use instanceof:

function logDate(value: Date | string) {
  if (value instanceof Date) {
    console.log(value.toISOString())
  } else {
    console.log(value)
  }
}

Discriminated unions

The pattern I use most is a shared kind field. Each variant in the union carries a literal type on that field.

type ApiResult =
  | { kind: 'ok', data: { name: string } }
  | { kind: 'error', message: string }

function handle(result: ApiResult) {
  switch (result.kind) {
    case 'ok':
      console.log(result.data.name)
      break
    case 'error':
      console.log(result.message)
      break
  }
}

TypeScript connects kind: 'ok' to the data property and kind: 'error' to message. No extra checks needed inside each branch.

This pairs well with never for exhaustive switches, which we cover in any vs unknown vs never.

Custom type guards with is

You can write a function that tells TypeScript what type a value is:

type Fish = { swim: () => void }
type Bird = { fly: () => void }

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined
}

function move(pet: Fish | Bird) {
  if (isFish(pet)) {
    pet.swim()
  } else {
    pet.fly()
  }
}

The pet is Fish return type is the type guard. When the function returns true, TypeScript narrows pet to Fish.

Custom guards help when the check is too complex for a one-line typeof or in test, or when you repeat it in many places.

Which one to reach for

My order: typeof for primitives, a discriminated union for object states I control, in for object shapes I do not control, instanceof for class instances and errors, and an is guard when the same check shows up more than twice.

If you validate data at runtime too, libraries like Zod narrow types after parsing. For compile-time unions you control, discriminated unions and is guards are the tools you reach for first.

Lesson completed