Type Narrowing with typeof in TypeScript
TypeScript uses JavaScript's typeof operator to narrow union types at compile time. Learn which typeof strings TypeScript recognizes, how narrowing works in if branches, and the typeof null trap.
TypeScript treats the JavaScript typeof operator as a type guard. Writing if (typeof x === "string") narrows the value to a string inside the true branch and removes string from the union in the false branch.
This narrowing is pure compile-time behavior, so nothing extra runs at runtime beyond the typeof check itself.
function format(value: string | number) {
if (typeof value === "string") {
return value.toUpperCase(); // value is string
}
return value.toFixed(2); // value is number
}
console.log(format("hello")); // "HELLO"
console.log(format(3.14159)); // "3.14"The typeof check splits the union. TypeScript tracks the narrowed type through each branch automatically.
The Eight Recognized typeof Strings
TypeScript recognizes eight typeof return values for narrowing, matching JavaScript's actual runtime behavior exactly.
| typeof return | TypeScript narrows to |
|---|---|
| "string" | string |
| "number" | number |
| "bigint" | bigint |
| "boolean" | boolean |
| "symbol" | symbol |
| "undefined" | undefined |
| "object" | object or null |
| "function" | Function |
Any other typeof comparison does not narrow the type. TypeScript only recognizes these eight strings.
How Narrowing Flows Through Branches
TypeScript tracks narrowing through if/else, return, and control flow:
function process(input: string | number | boolean) {
if (typeof input === "string") {
console.log(input.toUpperCase());
return;
}
if (typeof input === "number") {
console.log(input.toFixed(2));
return;
}
console.log(input ? "Yes" : "No"); // only boolean remains
}After the two typeof checks and early returns, only the boolean case remains. TypeScript understands control flow and removes types as each branch is exhausted.
Each check peels off one member of the union. The remaining type at the end is whatever was not yet handled.
The typeof null Trap
JavaScript has a historical quirk: typeof null returns "object". TypeScript knows this and adjusts accordingly.
function printAll(strs: string | string[] | null) {
if (typeof strs === "object") {
strs.forEach(s => console.log(s)); // Error!
}
}The object branch looks safe, but TypeScript still rejects the call inside it, because null was never actually eliminated from the type:
'strs' is possibly 'null'.TypeScript narrows the "object" check to a string array or null, not a string array alone. The null case is still present because typeof null also returns "object".
The fix is to check for null first, or combine the check with a truthiness test:
function printAll(strs: string | string[] | null) {
if (strs && typeof strs === "object") {
strs.forEach(s => console.log(s)); // strs is string[]
} else if (typeof strs === "string") {
console.log(strs);
}
}The truthiness check removes null first, since null is falsy, so the following typeof check now only matches the array.
Narrowing with typeof and Arrays
The typeof operator does not distinguish arrays from plain objects, since both return "object". Narrowing arrays needs Array.isArray instead:
function handle(data: string | string[]) {
if (Array.isArray(data)) {
data.forEach(item => console.log(item)); // data is string[]
} else {
console.log(data.toUpperCase()); // data is string
}
}TypeScript recognizes Array.isArray as a type guard, just like typeof.
Narrowing Functions
The typeof operator returns "function" for all function values, and TypeScript narrows to the built-in Function type:
function maybeCall(fn: (() => void) | string) {
if (typeof fn === "function") {
fn(); // fn is () => void
} else {
console.log(fn); // fn is string
}
}Narrowing with typeof and else if Chains
When a value could be several types, chain typeof checks with else if:
function describe(value: string | number | boolean | undefined) {
if (typeof value === "string") {
return `String of length ${value.length}`;
} else if (typeof value === "number") {
return `Number: ${value}`;
} else if (typeof value === "boolean") {
return `Boolean: ${value}`;
} else {
return "Undefined";
}
}Each else if branch removes the previously-checked type from the union, so the final else only handles whatever type is left.
Common Mistakes
Assuming typeof narrows arrays. The typeof result for an array is "object", not "array". Use Array.isArray instead.
Assuming typeof narrows null away. typeof null returning "object" is a JavaScript fact, not a bug. Always combine with a null check when needed.
Using typeof with non-recognized strings. Comparing against a made-up string like "array" does not narrow, because TypeScript does not recognize it as a valid typeof return value.
Forgetting that typeof is a runtime operator. The typeof check itself is not erased by the compiler. The narrowing is compile-time, but the comparison still executes in JavaScript every time the code runs.
Writing typeof outside a conditional. typeof only narrows when used directly in an if, switch, ternary, or similar control flow construct. A standalone typeof expression does not narrow anything.
const t = typeof value; // This does NOT narrow 'value'The assignment stores the result, but TypeScript does not track the type from the variable.
Combining typeof with Other Guards
typeof works well alongside other narrowing techniques, such as instanceof for class instances:
function handle(value: string | number | Date) {
if (typeof value === "string") {
console.log(value.toUpperCase());
} else if (typeof value === "number") {
console.log(value.toFixed(2));
} else {
console.log(value.toISOString());
}
}After removing string and number with typeof, only Date remains. No special instanceof check is needed for the last type, since it is whatever was not yet handled.
For narrowing non-primitive types like classes, see type narrowing with instanceof. For narrowing object shapes by property existence, see use the in operator for type narrowing.
Rune AI
Key Insights
- typeof checks inside if statements are type guards that narrow union types at compile time.
- TypeScript recognizes eight typeof return strings for narrowing.
- The true branch narrows to the checked type; the else branch removes it from the union.
- typeof null === 'object', so typeof 'object' narrows to object | null, not just object.
- Combine typeof with other checks like truthiness for multi-step narrowing.
Frequently Asked Questions
How does TypeScript narrow types with typeof?
Which typeof strings does TypeScript recognize for narrowing?
Does typeof null narrow correctly in TypeScript?
Conclusion
typeof narrowing is the most common type guard in TypeScript. Write a typeof check inside an if statement, and TypeScript narrows the type in each branch automatically. Watch out for typeof null === 'object' and always verify that the narrowed type matches what you expect at runtime.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.