Type Narrowing Basics in TypeScript

Type narrowing is how TypeScript refines a wide union type to a more specific one based on runtime checks. Learn typeof, truthiness, equality, and in-operator narrowing.

6 min read

TypeScript type narrowing refines a wide union type to a more specific one by following the runtime checks you write. When you check typeof, TypeScript knows the exact type inside that branch.

Type narrowing flow from union to specific

The diagram shows the core idea: a union type splits into specific types based on a runtime check. Each code branch knows exactly which type it is working with. The compiler traces your control flow and narrows wherever possible.

Why Narrowing Exists

Without narrowing, union types would be hard to use. The compiler only allows operations that work on every member of a union.

If you have a value that is either a string or a number, you cannot call string methods on it until you prove it is a string.

typescripttypescript
function printLength(value: string | number) {
  console.log(value.length);
  // Error: 'length' does not exist on 'string | number'
}

You fix this by narrowing. A typeof check tells TypeScript which branch handles which type.

Inside the if block, the compiler knows the value is a string. Inside the else block, it knows the value is a number. The runtime check doubles as a compile-time type refinement.

The Main Narrowing Techniques

Typeof Narrowing

The typeof operator is the most common type guard for primitives. TypeScript recognizes checks against the strings "string", "number", "bigint", "boolean", "symbol", "undefined", "object", and "function". Each typeof check removes the matched type from the remaining union.

typescripttypescript
function process(value: string | number | boolean) {
  if (typeof value === "string") {
    return value.toUpperCase();
  }
  if (typeof value === "number") {
    return value.toFixed(2);
  }
  return value;
}

After two checks, only boolean is left. TypeScript knows this and lets you return the value without further checks. Be careful with typeof and object: in JavaScript, typeof null returns "object", so that check keeps null in the narrowed type.

Truthiness Narrowing

In JavaScript, values like zero, empty string, null, and undefined are falsy. TypeScript uses truthiness checks in conditionals to narrow types. This is a quick way to guard against missing values.

typescripttypescript
function printMessage(msg?: string) {
  if (msg) {
    console.log(msg.toUpperCase()); // msg is string
  }
}

Inside the if block, TypeScript removes undefined, null, and empty string because those values are falsy. But truthiness can hide bugs: an empty string takes the same branch as undefined. If treating an empty string differently matters, use an explicit equality check instead.

Equality Narrowing

Strict and loose equality operators all narrow types. TypeScript understands that if two values are equal, their types must overlap. Checking against a specific value removes other possibilities from the union.

typescripttypescript
function handle(value: string | null) {
  if (value !== null) {
    console.log(value.toUpperCase()); // value is string
  }
}

This is more precise than truthiness because it distinguishes null from empty string. A useful shortcut: loose equality with null removes both null and undefined from a type. TypeScript understands this pattern and narrows correctly.

The In Operator

The in operator checks whether an object has a specific property. TypeScript uses it to narrow between object types in a union. This pattern is the foundation for discriminated unions.

typescripttypescript
type Cat = { meow: () => void };
type Dog = { bark: () => void };
 
function speak(animal: Cat | Dog) {
  if ("meow" in animal) {
    animal.meow(); // animal is Cat
  } else {
    animal.bark(); // animal is Dog
  }
}

After the in check, TypeScript knows which type applies in each branch. The if branch gets Cat, the else branch gets Dog. No type assertions needed.

Control Flow Analysis

Narrowing goes beyond individual if checks. TypeScript performs control flow analysis across the entire function, tracking how types change at each point. Early returns are the clearest example of this.

typescripttypescript
function padLeft(padding: number | string, input: string) {
  if (typeof padding === "number") {
    return " ".repeat(padding) + input;
  }
  return padding + input; // padding is string here
}

TypeScript knows that after the return, the number case is handled. Any code after the if block sees padding as string only.

This also works with throw, break, and continue. The compiler removes types from branches that are no longer reachable.

Narrowing vs Type Assertions

Narrowing is always preferable to a type assertion. Narrowing uses real runtime checks that TypeScript understands and verifies. Type assertions bypass checking entirely and can hide real bugs.

ApproachHow it worksSafety
NarrowingRuntime check (typeof, ===) narrows typeSafe: compiler verifies the check
Type assertion (as)Forces a type with no runtime checkUnsafe: no verification, can hide errors

Prefer narrowing. It keeps your types honest and your runtime behavior predictable. For more on when annotations are needed instead of inference, see When to add type annotations in TypeScript.

What Narrowing Cannot Do

Narrowing only works with checks TypeScript understands. A custom function that returns a boolean does not narrow types because the compiler does not look inside function bodies to determine their narrowing effect.

typescripttypescript
function isString(value: unknown): boolean {
  return typeof value === "string";
}
 
function process(value: string | number) {
  if (isString(value)) {
    console.log(value.toUpperCase());
    // Error: still 'string | number'
  }
}

For this, you need a custom type guard using the parameter is Type return syntax. That is covered in the full unions and narrowing section.

For the reverse process, where TypeScript expands narrow types into wider ones, see Type widening in TypeScript. For narrowing with discriminated unions, see Discriminated unions in TypeScript.

Rune AI

Rune AI

Key Insights

  • Narrowing refines a union type to a more specific type inside a code branch.
  • typeof is the most common narrowing check for primitives.
  • Truthiness checks remove null and undefined from a type.
  • Equality checks like === narrow both sides of the comparison.
  • The in operator narrows object types by checking for property existence.
RunePowered by Rune AI

Frequently Asked Questions

Does type narrowing affect runtime behavior?

No. Narrowing is purely compile-time. It helps TypeScript understand your code's control flow so it can type-check each branch correctly. The JavaScript output is unchanged.

What is the difference between narrowing and type assertions?

Narrowing uses runtime checks (typeof, ===, etc.) that TypeScript understands to refine types safely. Type assertions (as) force a type without any check and can hide real errors.

Can I create my own type narrowing functions?

Yes. These are called custom type guards and use the `parameter is Type` return syntax. They are covered in the full unions and narrowing section.

Conclusion

Type narrowing is TypeScript's way of following your runtime checks to refine types. When you write typeof x === 'string', the compiler knows x is a string inside that branch. The same works for truthiness checks, equality, and the in operator. Narrowing is not something you turn on -- it happens automatically as you write normal JavaScript checks. Understanding it helps you write safer code with fewer type assertions and more accurate types.