Narrowing: How TypeScript Thinks
Beginner · 22 min read · ▶ live playground · ✦ checkpoint
Union types give you flexibility — string | number says "either one." But flexibility has a price: you can only use what both could do. Narrowing is how you pay it back. Write an ordinary if, and the checker follows your logic like a detective, eliminating suspects line by line — this is the lesson where TypeScript stops feeling like paperwork and starts feeling like a colleague.
A union is a promise
When a value's type is string | number, TypeScript holds you to the honest reading: it could be either. So it only lets you do things that are safe for every member of the union. .toFixed() exists on numbers but not strings — so on the union, it's an error:
function printId(id: string | number) {
console.log(id.toFixed(2)); // not safe for the string case
}→ error TS2339: Property 'toFixed' does not exist on type 'string | number'. Property 'toFixed' does not exist on type 'string'.
Read the two-part message closely: the checker even names the member that breaks the deal. The union promised "maybe a string," and strings can't toFixed.
typeof — teaching the checker with an if
Here's the beautiful part: TypeScript already understands the JavaScript you'd naturally write. Check typeof, and inside that branch the union shrinks:
function describe(value: string | number | boolean): string {
if (typeof value === "string") {
return value.trim(); // value: string here
}
if (typeof value === "number") {
return value.toFixed(2); // value: number here
}
return value ? "yes" : "no"; // value: boolean — the only one left
}
console.log(describe(" padded "));
console.log(describe(3.14159));
console.log(describe(true));→ padded / 3.14 / yes — hover value on each line in your editor: the type is different every time. Same variable, three types.
Notice the last line needs no check at all. The checker did the elimination: not a string, not a number, and the union only had three members — what remains must be boolean. Narrowing works in both directions: proving what something is, and proving what it no longer can be.
Try it — watch a union shrink
Hit Run — it type-checks first, and if the check fails, nothing runs, exactly like tsc on your machine. Then type your own statements at the > prompt — try format("all good"), then format(true) and read the real diagnostic. Variables and types you create carry over between entries.
Truthiness, equality & early returns
Narrowing isn't only typeof. Any check the checker can follow narrows the type — truthiness, === comparisons, and the early-return pattern you'll use daily:
function greet(name: string | undefined): string {
if (name === undefined) {
return "Hello, stranger";
}
return "Hello, " + name.toUpperCase(); // name: string — undefined was returned away
}
console.log(greet(undefined));
console.log(greet("ada"));→ Hello, stranger / Hello, ADA — after the early return, undefined is eliminated for the rest of the function. Guard clauses ARE narrowing.
The truthiness blind spot
Truthiness narrowing — if (value) … — is tempting shorthand, but several perfectly valid values are falsy, and two of them are often real data:
if (count)skips0— a perfectly valid count.if (message)skips""— a perfectly valid empty string.- Prefer the explicit check:
if (count !== undefined).
Checkpoint
Answer all three to mark this lesson complete