TypeScript Union Types and Narrowing

Accept more than one type and narrow it safely with typeof.

TypeScript
type Id = number | string;

function format(id: Id): string {
  if (typeof id === "number") {
    return `#${id.toFixed(0)}`;
  }
  return id.toUpperCase();
}

console.log(format(42));
console.log(format("ab7"));
Output
#42
AB7

Explanation

A union type such as number | string says a value may be any one of the listed types. You can only use operations that are valid for every member until you narrow the type.

Inside an if that tests typeof id === "number", TypeScript knows id is a number, and in the rest of the function it knows id is a string. This is called narrowing, and it lets one function handle several kinds of input without casts.

Explore the dry run
Guided walkthroughReady
1type Id = number | string;
2
3function format(id: Id): string {
4 if (typeof id === "number") {
5 return `#${id.toFixed(0)}`;
6 }
7 return id.toUpperCase();
8}
9
10console.log(format(42));
11console.log(format("ab7"));

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
Id can hold either a number or a string.—
—
Test the runtime type; inside this block id is a number.—
—
42 is a number, so the first branch formats it.id = 42
#42
The string reaches the last line, where id is known to be a string.id = "ab7"
#42
AB7

Cost and common mistakes

Narrowing is checked at compile time. Each call is O(1).

  • Calling toUpperCase on the whole union is a compile error; narrow first.
  • typeof null is "object", so narrow null and undefined with explicit checks.

Try it yourself

Predict format(7.9).

Reveal answer and reasoning

toFixed(0) rounds to "8", so it prints #8.

Further reading