Latticework

Command Palette

Search for a command to run...

TypeScript

Narrowing

14 min

Explanation

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
Try it

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

Loading editor…
Exercise

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)`.

Quiz

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?

Checkpoint

You can write functions over union types and use typeof checks to narrow them, letting TypeScript verify each branch is type-safe.