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 result | Narrowed 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". UseArray.isArray(x)to narrow to an array type —typeofcannot 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
typeoffirst when narrowing unions of primitives — it is the simplest, runtime-faithful guard. - Only compare against the eight valid
typeofstrings; typos become compile errors, which is a feature. - Always handle
nullseparately after atypeof x === "object"check. - Use
Array.isArray()rather thantypeofto detect arrays. - Let control-flow analysis do the work: early
returnin a branch narrows the rest of the function automatically. - Combine
typeofwith other guards (in,instanceof) when a single check cannot fully discriminate.