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.

6 min read

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.

typescripttypescript
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 returnTypeScript 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:

typescripttypescript
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.

Multi-step narrowing with typeof and control flow

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.

typescripttypescript
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:

texttext
'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:

typescripttypescript
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:

typescripttypescript
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:

typescripttypescript
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:

typescripttypescript
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.

typescripttypescript
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:

typescripttypescript
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

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.
RunePowered by Rune AI

Frequently Asked Questions

How does TypeScript narrow types with typeof?

When you write if (typeof x === 'string'), TypeScript treats that check as a type guard. In the true branch, x is narrowed to string. In the false branch, string is removed from the union. This is compile-time only.

Which typeof strings does TypeScript recognize for narrowing?

TypeScript recognizes: 'string', 'number', 'bigint', 'boolean', 'symbol', 'undefined', 'object', and 'function'. These match JavaScript's typeof operator return values.

Does typeof null narrow correctly in TypeScript?

JavaScript's typeof null returns 'object', which is a known quirk. TypeScript accounts for this: typeof x === 'object' narrows to object | null, not just object. You need an additional null check to fully narrow.

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.