Skip to content
TypeScript ts narrowing 3 min read

typeof Narrowing

When a value can be one of several types, TypeScript lets you write ordinary runtime checks and then narrows the static type inside each branch to match. The typeof operator is the most common starting point: because JavaScript’s typeof returns a known set of strings at runtime, the compiler can use a typeof comparison to refine a union type. This page covers which strings typeof recognizes, how narrowing flows through if/else branches, and the gotchas around null, arrays, and functions.

The typeof type guard

TypeScript understands typeof x === "..." as a type guard. Inside the matching branch, the variable is narrowed to the corresponding type, so member access and operations specific to that type become legal.

function formatValue(value: string | number): string {
  if (typeof value === "string") {
    // value: string
    return value.trim().toUpperCase();
  }
  // value: number
  return value.toFixed(2);
}

Because the if branch handles the string case and returns, the compiler knows that any code after it can only be the number case. This is control-flow analysis: TypeScript tracks the narrowed type at every point in the function body.

The recognized typeof strings

JavaScript’s typeof only ever returns a fixed set of strings, and TypeScript maps each to a type. Comparing against any other string is flagged as an error because it can never be true.

typeof resultNarrowed type
"string"string
"number"number
"bigint"bigint
"boolean"boolean
"symbol"symbol
"undefined"undefined
"object"object, array, or null
"function"a callable type
function check(x: unknown) {
  // ❌ Error: This comparison appears to be unintentional
  if (typeof x === "strng") {
  }
}

Narrowing across multiple branches

typeof chains naturally with else if. Each branch refines the union further, and the compiler keeps subtracting handled members from what remains.

type Primitive = string | number | boolean;

function describe(value: Primitive): string {
  if (typeof value === "string") {
    return `text(${value.length})`;
  } else if (typeof value === "number") {
    return `num(${value.toFixed(1)})`;
  } else {
    // value: boolean
    return value ? "yes" : "no";
  }
}

After the first two branches consume string and number, the else block can only be boolean, so value ? ... : ... is fully typed without any cast.

The “object” and null trap

The classic JavaScript wart is that typeof null === "object". TypeScript faithfully models this, so a typeof x === "object" check does not exclude null on its own — you must guard against it explicitly.

function getKeys(value: object | null): string[] {
  // ❌ value could still be null here
  if (typeof value === "object") {
    // value: object | null
    if (value === null) return [];
    return Object.keys(value);
  }
  return [];
}

Remember that arrays also report typeof === "object". Use Array.isArray(x) to narrow to an array type — typeof cannot distinguish arrays from plain objects.

Functions and typeof

typeof x === "function" narrows to a callable type, which is handy when a parameter may be a value or a factory that produces it.

type Lazy<T> = T | (() => T);

function resolve<T>(input: Lazy<T>): T {
  if (typeof input === "function") {
    // input: () => T
    return (input as () => T)();
  }
  // input: T
  return input;
}

The narrowing isolates the callable branch; the small assertion is needed only because T itself could theoretically be a function type, which TypeScript cannot rule out generically.

Best Practices

  • Reach for typeof first when narrowing unions of primitives — it is the simplest, runtime-faithful guard.
  • Only compare against the eight valid typeof strings; typos become compile errors, which is a feature.
  • Always handle null separately after a typeof x === "object" check.
  • Use Array.isArray() rather than typeof to detect arrays.
  • Let control-flow analysis do the work: early return in a branch narrows the rest of the function automatically.
  • Combine typeof with other guards (in, instanceof) when a single check cannot fully discriminate.
Last updated June 29, 2026
Was this helpful?