Narrowing
14 min
A union type (string | number) means a value could be either —
so TypeScript won't let you call a string-only method on it directly,
since it might be a number. Narrowing is how you resolve that: a
runtime check like typeof x === "string" proves to the compiler
that, INSIDE that branch, x can only be a string — so
x.toUpperCase() becomes valid there, with no cast or assertion
needed. This isn't a TypeScript-only trick — typeof/instanceof/
equality checks are real JavaScript that runs and branches exactly the
same way; TypeScript is just smart enough to track what each branch
proves.
function describe(x: string | number): string {
if (typeof x === "string") return "text:" + x.toUpperCase();
return "double:" + (x * 2); // TypeScript knows x is number here
}
console.log(describe("hi"));
// text:HI
Same function, two branches -- typeof id === 'number' narrows id to number in the first branch (so .toString()/.padStart() are valid there), and to string in the fallthrough (so .toUpperCase() is valid there).
Write `describe(x: string | number): string`. If `x` is a string, return `"text:" + <its uppercase form>`. Otherwise (it's a number), return `"double:" + (x * 2)`.
In `if (typeof x === "string") { x.toUpperCase(); }`, why does TypeScript let you call .toUpperCase() on x inside that block even though x's declared type is string | number?
You can write functions over union types and use typeof checks to narrow them, letting TypeScript verify each branch is type-safe.