typeof narrowing
Narrows a union by testing a value's runtime type tag with the typeof operator.
typeof operand === "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"- Added in TypeScript 1.4
Parameters
| Parameter | Type | Description |
|---|---|---|
$operand | unknown | The 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
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.
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.
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