Course outline · 0% complete

0/26 lessons0%

Course overview →

Narrowing with typeof and in

lesson 4-2 · ~11 min · 12/26

Proving which member you have

Narrowing is TypeScript following your runtime checks and shrinking a union accordingly. The workhorse is JavaScript's own typeof, which you met in the first JavaScript course:

function double(value: string | number): string | number {
  if (typeof value === "number") {
    return value * 2;      // here value is number
  }
  return value + value;    // here value must be string
}

Inside the if, TypeScript knows value is a number, so arithmetic is allowed. After the if, only string remains, so string concatenation is allowed. No casts, no new syntax. The compiler reads ordinary control flow and updates the type line by line. This is why the if (p.bio !== undefined) check in lesson 3-2 unlocked p.bio.

value: string | numbertypeof value === "number"?true: value is numberfalse: value is string
A typeof check splits the union: each branch of the if sees a smaller, more precise type.

Following both branches

function double(value: string | number): string | number {
  if (typeof value === "number") {
    return value * 2;
  }
  return value + value;
}

console.log(double(21));
console.log(double("ha"));

Output

42
haha

The number branch goes through * 2, giving 42. The string branch adds the value to itself, giving "haha". Both are the natural meaning of "double" for their own type, which is why one function can serve both.

Moving return value * 2 below the if makes the compiler reject it, and the reason is worth stating plainly: outside the guard, value is once again string | number, and multiplication is not valid for a string. The narrowing was never a property of the variable, it was a property of the place in the code.

Narrowing objects with in

typeof only distinguishes primitives, it answers "object" for every object. To split a union of object shapes, check which property exists with the in operator:

interface Circle { radius: number }
interface Square { side: number }

function area(shape: Circle | Square): number {
  if ("radius" in shape) {
    return 3.14 * shape.radius * shape.radius;
  }
  return shape.side * shape.side;
}

"radius" in shape is plain JavaScript, and TypeScript understands it: inside the if the shape is a Circle, after it a Square. It works, but hunting for distinguishing properties gets fragile as shapes grow. The next lesson shows the pattern real codebases use instead.

toLabel: a different method in each branch

toLabel(input: string | number) formats its argument differently depending on which member it received, calling a number method in one branch and a string method in the other.

function toLabel(input: string | number): string {
  if (typeof input === "number") {
    return "num:" + input.toFixed(1);
  }
  return "str:" + input.toUpperCase();
}

console.log(toLabel(3.14));
console.log(toLabel("hi"));

Output

num:3.1
str:HI

What narrowing unlocks here

  • The function opens with if (typeof input === "number") and returns inside it, which is the shape most narrowing takes: check, handle, return.
  • toFixed and toUpperCase only compile inside the branch where TypeScript knows the precise type. Swap them between branches and both lines fail, since neither method exists on the other type.
  • toFixed(1) rounds 3.14 to one decimal place and returns a string, which is why the output reads num:3.1 rather than num:3.14.