skip to content

typeof, instanceof, and in Narrowing

The three classic guards: typeof for primitives, instanceof for class instances, and `in` for discriminating by property presence. Interviewers ask because each has a blind spot — typeof null reporting "object", instanceof failing across realms, `in` on optional properties.

part ofTypeScriptoverview, primer and where to startread it →
on this pageshow

questions

5

In TypeScript, given `function fmt(x: string | number)`, what type does the checker give `x` inside `if (typeof x === 'string')` and in the `else` branch, and which strings is the compiler willing to accept on the right-hand side of a `typeof` comparison?

level: juniorimportance: must knowfreq 78%

answer

  1. the checker reads the runtime check
  2. branch filters the declared union
  3. only the tags typeof can return
  4. literal members survive the filter

basics

~20 s

TypeScript narrows x to string inside the if branch and to number in the else branch. Only the eight strings typeof can actually return are accepted; comparing against any other string is reported as a comparison with no overlap.

solid answer

~50 s

A `typeof` test is one of the runtime expressions the checker reads as a type guard. Inside `if (typeof x === 'string')` the compiler keeps only the union members whose `typeof` result could be `'string'`, so `x` is `string`; the `else` branch keeps the remainder, so `x` is `number`. The compiler types the expression `typeof x` as the union of literal strings `typeof` can produce — `'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function'` — so comparing it to something like `'array'` is flagged as a comparison with no overlap rather than silently never matching. Narrowing filters the declared union; it never invents a member, so `'a' | 'b' | number` narrows to `'a' | 'b'`, not to `string`. And all of it is erased: the `if` survives compilation, the refinement does not.

code

typescript · 14 lines
typescript
function fmt(x: string | number): string {
  if (typeof x === "string") {
    return x.toUpperCase(); // x: string
  }
  return x.toFixed(2); // x: number
}

function pick(x: "a" | "b" | number): string {
  if (typeof x === "string") {
    // x is "a" | "b" here, not string
    return x;
  }
  return String(x);
}

go deeper

for a junior

Be ready to state the type in both branches of a typeof guard on a simple union, and to say plainly that arrays and null both report "object", so typeof cannot pick an array out.

for a middle

Explain the mechanics: the checker types the typeof expression as a union of the eight possible tags, then filters the declared union in each branch, preserving literal members rather than widening them.

for a senior

Show how you use typeof chains to make an unknown boundary value safe without assertions, and how an exhausted chain turns a later change to the data's shape into a compile error instead of a runtime one.

for a principal

Own the guidance on where guards belong: parse and narrow once at the system's edge into a well-typed model, rather than scattering typeof checks through business logic where each one is a place the model was not trusted.

## What a typeof guard actually does TypeScript's type layer is erased when the code is emitted, so the checker cannot insert runtime checks of its own. Instead it learns to *read* a handful of ordinary JavaScript expressions and treat them as evidence about a value's type. `typeof x === '...'` is the first and most common of these. When control-flow analysis reaches such a condition, it splits the type of the tested reference in two: the true branch keeps the union members whose `typeof` result could be the compared string, and the false branch keeps the rest. ```ts function fmt(x: string | number): string { if (typeof x === "string") { return x.toUpperCase(); // x: string } return x.toFixed(2); // x: number } ``` Both calls type-check only because of the split. Without the guard, `x.toUpperCase()` is an error: `toUpperCase` does not exist on `string | number`, because a union only offers the members every constituent has. ## The set of strings the compiler accepts The compiler gives the expression `typeof x` the type `"string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"`. That is not a lint rule — it is the declared type of the operator's result. Comparing a value of that type to a string literal outside the set produces a type error saying the two types have no overlap: ```ts declare const x: unknown; // Error: this comparison appears to be unintentional — // the two operand types have no overlap. if (typeof x === "array") { } ``` This catches the single most common beginner mistake, the expectation that arrays report `"array"`. They do not; arrays are objects. ## What each tag narrows to Starting from a union, each tag keeps the constituents compatible with it: - `"string"`, `"number"`, `"boolean"`, `"bigint"`, `"symbol"` keep the corresponding primitive constituents, *including literal types*. For `x: "a" | "b" | number`, the true branch of `typeof x === "string"` is `"a" | "b"`. Narrowing filters what was declared; it never widens to a type the declaration did not contain. - `"undefined"` keeps `undefined`. - `"function"` keeps the callable constituents. - `"object"` keeps the object-like constituents — and, when `strictNullChecks` is on, `null` as well, because that is what the runtime check really does. Starting from `unknown` the behaviour is slightly different, because there is no declared union to filter. Here the guard *produces* a type: `typeof v === "string"` gives `string`, `typeof v === "function"` gives `Function`, and `typeof v === "object"` gives `object | null`. This is why a `typeof` chain is the standard way to make an `unknown` value — the type of a caught error, or of parsed JSON — usable without an assertion. ## Exhaustive chains Because each branch removes constituents, a chain of guards eventually leaves nothing: ```ts function describe(v: string | number | boolean): string { if (typeof v === "string") return "text"; if (typeof v === "number") return "count"; return v ? "yes" : "no"; // v: boolean } ``` If a fourth member is later added to the parameter's union, the final branch stops being `boolean` and the code that assumed it will fail to compile — narrowing turns a data-shape change into a compile error, which is most of the point. ## Erasure The emitted JavaScript still contains the `if (typeof x === "string")`. What disappears is the *refinement*: there is no runtime record that the compiler believed `x` was a `string` in that branch. The guard is genuine executable JavaScript that the checker happens to understand; the narrowed types are bookkeeping that ends at compile time. That is also why `typeof` narrowing is limited to what `typeof` can distinguish — it can separate a string from a number, but it can never separate two interfaces, since both report `"object"`. ## Interview framing Say what the branches are, say that the tested expression is typed as a union of the eight tags so a bogus tag is a compile error, and mention that narrowing filters the declared union rather than replacing it. Adding that all of this vanishes at emit shows you understand which layer you are working in.

  • If `x` is declared `'a' | 'b' | number`, what exactly is `x` inside `if (typeof x === 'string')` — and why not `string`?
    It is `'a' | 'b'`. Narrowing is a filter over the constituents that were declared, not a replacement type: the checker keeps every member whose `typeof` could be `'string'` and drops the rest. Widening it to `string` would *lose* information and would let you assign `'c'` inside the branch, so the compiler keeps the literal union intact.
  • What does a `typeof` guard give you when you start from `unknown` rather than from a union?
    With no declared union to filter, the guard produces a type outright: `'string'` yields `string`, `'number'` yields `number`, `'function'` yields `Function`, and `'object'` yields `object | null`. That is the normal way to make an `unknown` value — a caught error, or parsed JSON — usable without reaching for an assertion, which would perform no check at all.

saying these in an interview costs you the question

  • Claims typeof returns "array" for arrays
  • Thinks the narrowing survives into the emitted JavaScript
  • Expects typeof to distinguish two different interfaces
  • Says narrowing widens literal members to string
  • Believes an unrecognised typeof string just silently never matches

context

open as a page

In TypeScript, how does `if ('radius' in shape)` narrow a union of object types, and why can an optional property leave the member you expected to be removed sitting in the `else` branch?

level: middleimportance: must knowfreq 58%

basics

~20 s

The in operator keeps union members that declare the named property and drops those that do not. When the property is optional, the member can legitimately lack it at runtime, so the checker keeps that member in both branches.

open as a page

In a TypeScript project with `strictNullChecks` on, a value is declared `string[] | string | null`. Which branch of `if (typeof value === 'object')` does the checker put `null` in, what is the narrowed type there, and how do you exclude it?

level: middleimportance: must knowfreq 62%

basics

~20 s

Null lands in the true branch: the checker narrows to string[] | null, because the runtime check it models reports "object" for null. Combine the guard with an explicit non-null check, or test for the primitive member instead.

open as a page

In TypeScript, what type does `if (Array.isArray(x))` narrow `x: unknown` to, and why is that a hole in an otherwise strict codebase?

level: middleimportance: should knowfreq 38%

basics

~20 s

It narrows to any[], because the standard library declares the check with the predicate arg is any[]. Every element read out of it is any, so a strict codebase silently loses type safety inside the branch.

open as a page

In TypeScript, `pet` is declared `Dog | Cat`, where both classes declare exactly the same members. Why does `if (pet instanceof Dog)` fail to narrow `pet` to `Dog`, and what change makes the narrowing work?

level: seniorimportance: should knowfreq 42%

basics

~20 s

TypeScript's type system is structural, so two classes with identical members are interchangeable types. Narrowing keeps every union member compatible with the target, which is both of them. Giving one class a private or #private field makes them incompatible and restores the narrowing.

open as a page