Narrowing uncertain values

Narrow with typeof

Refine a primitive union by checking the runtime kind before using type-specific operations.

A union describes every value allowed at a boundary. Inside the function you often need to know which one you actually got. A string | number cannot call toFixed() or trim() until you find out.

This is what happens if you try anyway:

function format(value: string | number) {
  return value.toFixed(2)
}
Property 'toFixed' does not exist on type 'string | number'.
  Property 'toFixed' does not exist on type 'string'.

The second line is the useful one. TypeScript is not saying toFixed() never exists. It is saying it does not exist on one of the two members, and the code has to work for both.

A runtime check fixes it. typeof tells TypeScript which member exists on the current path:

function format(value: string | number) {
  return typeof value === 'number' ? value.toFixed(2) : value.trim()
}

In the first branch value is a number, so toFixed() is fine. In the other branch it must be a string, so trim() is fine. This is narrowing: the type gets smaller as your code proves things about the value.

Call format(3.14159) and you get '3.14'. Call format(' hello ') and you get 'hello'.

The check is real JavaScript

typeof runs at runtime, exactly as it does in plain JavaScript. TypeScript follows the control flow of your code and updates the type in each branch. Nothing extra is emitted. Move the toFixed() call before the check and the error comes back, because at that point TypeScript has no proof yet.

This is why narrowing feels natural once you get it. You write the check you would have written anyway, and the compiler reads it.

One edge case to remember

typeof null is 'object'. If you check typeof value === 'object' to find an object, null slips through and the next property access crashes. Pair the object check with value !== null. We use that exact pair in the type predicate lesson later.

Also, typeof only distinguishes primitives. Its answers are 'string', 'number', 'boolean', 'bigint', 'symbol', 'undefined', 'object', and 'function'. Two different object shapes both answer 'object', so for those you need the checks in the next lessons.

Try this: extend format() to accept boolean as well, add a branch that returns 'yes' or 'no', and hover over value in every branch to watch the type shrink.

Lesson completed