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?
answer
- the checker reads the runtime check
- branch filters the declared union
- only the tags typeof can return
- literal members survive the filter
basics
~20 sTypeScript 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 sA `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 linesfunction 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
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.
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.
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.
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