Overview

Narrowing turns a wide type into a usable one inside a branch. The compiler narrows on guards, discriminants, and assertion functions; everything else is a cast. Strict mode makes wide types unsafe to use, which is why narrowing matters (typescript-strict-mode). For a lookup table of patterns, see typescript-narrowing-patterns.

Use typeof for primitives and instanceof for classes

typeof returns one of eight strings: "string", "number", "bigint", "boolean", "symbol", "undefined", "object", "function". typeof null is "object", so rule out null with value != null or === null.

instanceof narrows to a class instance and is the usual guard after catch (the variable is unknown). It fails across realms (iframes, vm contexts) and on plain object literals; use a discriminant for those.

try {
  await loadInvoice(id)
} catch (err) {
  if (err instanceof HttpError) return { status: err.status }
  throw err
}

Use in only for two shapes without a tag

"meow" in animal narrows Cat | Dog when only one has the property. With three or more shapes, add a discriminant field instead.

Prefer discriminated unions

A literal-typed tag (kind, type, status) narrows by equality, and each case sees exactly its variant. End the switch with a never assignment so adding a variant breaks every switch that forgot it.

function label(inv: Invoice): string {
  switch (inv.status) {
    case "draft": return "Draft"
    case "sent": return `Sent ${inv.sentAt.toISOString()}`
    case "paid": return `Paid ${inv.paidAt.toISOString()}`
    default: {
      const unhandled: never = inv
      throw new Error(`Unhandled: ${JSON.stringify(unhandled)}`)
    }
  }
}

Let inference write simple predicates

Since TypeScript 5.5 the compiler infers a type predicate from a function that returns a boolean check, so .filter narrows without an annotation.

const defined = items.filter((x) => x !== undefined)  // T[] in 5.5+, (T | undefined)[] before

Write value is T yourself only when the check is not a simple comparison, and keep the body honest: the compiler trusts it. For untrusted input (HTTP, files, env) a predicate that checks two fields is not validation; use a schema (typescript-runtime-validation).

function isInvoice(v: unknown): v is Invoice {
  return typeof v === "object" && v !== null && "status" in v
}

Use assertion functions for invariants

asserts value is T throws on failure and narrows after the call. Use it where you know something the type system does not, such as the first row of a non-empty result under noUncheckedIndexedAccess.

function assertDefined<T>(v: T | undefined, msg: string): asserts v is T {
  if (v === undefined) throw new Error(msg)
}

Return early instead of re-checking

After if (value === null) return "", the rest of the function already sees string. Re-checking !== null inside makes the reader prove the narrowing twice. Use NonNullable<T> (typescript-utility-types) when you need to name the narrowed type.