typeof narrowing

Narrows a union by testing a value's runtime type tag with the typeof operator.

Signature
typeof operand === "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
  • Added in TypeScript 1.4

Parameters

ParameterTypeDescription
$operandunknownThe expression being tested. Narrowing only applies when it is a variable, parameter, or a dotted property path on a readonly/const reference.
$tag"string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"A string literal compared with === or !==. Comparing against any other literal is an error, and comparing against a non-literal string does not narrow.

Return value

booleanA boolean. In the true branch the operand narrows to the union members matching that tag; in the false branch those members are removed.

Errors

Nothing. `typeof` is the one operator that does not throw on an undeclared identifier. tsc errors if the compared literal is not one of the eight valid tags.

Changelog

  • PHP 1.4typeof type guards introduced alongside instanceof.
  • PHP 4.4A typeof check stored in a const is now tracked, so `const isStr = typeof x === "string"; if (isStr) { ... }` narrows.
  • PHP 5.3`switch (true)` with typeof checks in the case clauses narrows in each clause.

typeof is the cheapest narrowing there is: a runtime operator JavaScript already has, and the compiler models its eight possible results. Compare it against a string literal with === or !== and the union splits.

When to reach for it

  • Overloaded arguments — padding: number | string, id: string | number.
  • Separating a function from a value in an options bag (typeof opt === "function" ? opt() : opt).
  • Testing for "undefined" on a value that may not be declared at all, which no other operator survives.

What trips people up

typeof null === "object". This is a bug preserved from 1995 JavaScript, and TypeScript models it faithfully. Narrowing to "object" leaves null in the type, so value.length in that branch still errors under strictNullChecks. Pair it with an explicit value !== null.

Arrays are also "object", so typeof cannot separate string[] from Record<string, unknown> — use Array.isArray, which is declared as a type predicate.

Finally, the tag must be a literal in the comparison. Storing it in a variable first (const t = typeof x; if (t === "string")) narrows t, not x.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Accepting either a number of spaces or a literal padding string in one parameter
  • Letting an option be a value or a lazy factory function
  • Handling a value that may be string, string[] or null in one function

Frequently asked questions

Why is my value still possibly null after typeof x === "object"?

Because typeof null is "object" in JavaScript, and TypeScript models that faithfully. Add an explicit `x !== null` check, or put a truthiness test in front: `if (x && typeof x === "object")`.

How do I check if something is an array in TypeScript?

Use `Array.isArray(x)`, not typeof — arrays report "object". Array.isArray is typed as `arg is any[]`, so it narrows properly.

Why doesn't storing typeof x in a variable narrow x?

Before TypeScript 4.4 it never did. Since 4.4 a const holding the whole comparison (`const isStr = typeof x === "string"`) does narrow, but a const holding only the tag (`const t = typeof x`) still does not.

More examples

The classic padLeft, and the null trap side by side

Prints " x" then [ 'a', 'b' ] [ 'c' ] []. The `strs &&` in front of the object check is what removes null; drop it and TypeScript reports that `strs` is possibly null when you iterate it.

Example · typescript
function padLeft(padding: number | string, input: string): string {
  if (typeof padding === "number") {
    return " ".repeat(padding) + input;
  }
  return padding + input;
}

function printAll(strs: string | string[] | null): string[] {
  const out: string[] = [];
  // Wrong: typeof strs === "object" is true for null too.
  // Right: check truthiness first, then the object tag.
  if (strs && typeof strs === "object") out.push(...strs);
  else if (typeof strs === "string") out.push(strs);
  return out;
}

console.log(JSON.stringify(padLeft(3, "x")));
console.log(printAll(["a", "b"]), printAll("c"), printAll(null));

typeof cannot tell an array from an object

Prints 3, 4, 2, 0, 0. Arrays report typeof "object", so Array.isArray has to come first — it is declared in lib.es5.d.ts as `isArray(arg: any): arg is any[]`, a type predicate, which is why it narrows at all.

Example · typescript
function size(value: unknown): number {
  if (Array.isArray(value)) return value.length;      // value: any[]
  if (typeof value === "string") return value.length; // value: string
  if (typeof value === "object" && value !== null) {
    return Object.keys(value).length;                 // value: object
  }
  return 0;
}

console.log(size([1, 2, 3]));
console.log(size("abcd"));
console.log(size({ a: 1, b: 2 }));
console.log(size(null));
console.log(size(42));

Discussion

  • Be the first to comment on this lesson.