typeof Narrowing

Difficulty: Intermediate

Type narrowing is TypeScript's ability to refine a broad type to a more specific one based on control flow analysis. The most basic form of narrowing uses the JavaScript `typeof` operator. When you write `typeof x === 'string'`, TypeScript understands that within the `true` branch, `x` is a `string`, and in the `false` branch, it is whatever remains of the original type.

The `typeof` operator returns one of these strings at runtime: `'string'`, `'number'`, `'boolean'`, `'undefined'`, `'object'`, `'function'`, `'symbol'`, and `'bigint'`. TypeScript recognizes all of these as type guards. Note that `typeof null === 'object'` is a well-known JavaScript quirk, so `typeof` alone cannot distinguish between objects and null.

Truthiness narrowing is another common pattern. When you write `if (value)`, TypeScript narrows out `null`, `undefined`, `0`, `""`, `false`, and `NaN` from the type. This is useful for quickly checking whether an optional value exists. However, truthiness narrowing can be too aggressive - it also eliminates legitimate falsy values like `0` and `""`, so use it carefully.

Equality narrowing uses `===`, `!==`, `==`, and `!=` to narrow types. When you compare a union type value to a specific literal, TypeScript narrows within the matching branch. The `==` and `!=` operators are especially useful with `null` because `x == null` checks for both `null` and `undefined`, which is often exactly what you want. These narrowing mechanisms work together to provide type safety with zero runtime overhead - they use checks you would write anyway.

Code examples

typeof Narrowing in Action

function padLeft(value: string, padding: string | number): string {
  if (typeof padding === "number") {
    // padding is narrowed to number
    return " ".repeat(padding) + value;
  }
  // padding is narrowed to string
  return padding + value;
}

console.log(padLeft("Hello", 4));
console.log(padLeft("Hello", ">>> "));

After the typeof check, TypeScript knows padding is number in the if branch and string in the else branch, allowing you to call type-specific operations safely.

Truthiness Narrowing

function printName(name: string | null | undefined): string {
  if (name) {
    // name is narrowed to string (null and undefined removed)
    return name.toUpperCase();
  }
  return "Anonymous";
}

function getLength(value: string | string[] | null): number {
  if (!value) {
    return 0;
  }
  // value is narrowed to string | string[]
  return value.length;
}

console.log(printName("alice"));
console.log(printName(null));
console.log(getLength(["a", "b", "c"]));
console.log(getLength(null));

Truthiness checks narrow out null and undefined. In getLength, the !value check handles null, and the remaining code can safely access .length on string or string[].

Equality Narrowing

function compare(a: string | number, b: string | boolean): string {
  if (a === b) {
    // Both narrowed to string (the only common type)
    return `Both are the string: ${a.toUpperCase()}`;
  }
  return `Different: ${a} and ${b}`;
}

function handleValue(value: string | null | undefined): string {
  if (value == null) {
    // Catches both null and undefined
    return "No value";
  }
  return value.trim();
}

console.log(compare("hello", "hello"));
console.log(compare(42, true));
console.log(handleValue(null));
console.log(handleValue("  spaced  "));

When a === b, TypeScript narrows both to their common type (string). The == null check catches both null and undefined in a single comparison.

Key points

Concepts covered

typeof operator, type narrowing, truthiness narrowing, equality narrowing