What is type narrowing in TypeScript?

Type narrowing is how TypeScript refines a broad type to a more specific one inside conditional blocks using typeof, instanceof, in, or custom type guards.

TypeScript

Overview

When a variable has a broad type (like `string | number` or `unknown`), TypeScript cannot safely let you call string-only or number-only methods on it. Type narrowing is the process of writing checks that let TypeScript prove which specific type is in use inside a code block.

Key Points

  • •`typeof x === "string"` narrows to `string`; works for `number`, `boolean`, `symbol`, `bigint`, `function`, `undefined`.
  • •`instanceof x` narrows to a class instance type.
  • •`"key" in obj` narrows to types that have that key.
  • •Assignment and truthiness checks also narrow types.
  • •Custom type guard functions: `function isUser(x: unknown): x is User { ... }` - the return type `x is User` tells TypeScript what to narrow to.
  • •The `never` type represents an unreachable code path useful for exhaustive checks.

Examples

typeof narrowing

typescript
function format(value: string | number): string {
  if (typeof value === 'string') {
    return value.toUpperCase(); // TS knows: string
  }
  return value.toFixed(2);     // TS knows: number
}

instanceof narrowing

typescript
function logError(error: unknown) {
  if (error instanceof Error) {
    console.error(error.message); // TS knows: Error
    console.error(error.stack);
  } else {
    console.error('Unknown error:', error);
  }
}

in operator narrowing (discriminated union)

typescript
type Circle = { kind: 'circle'; radius: number };
type Square = { kind: 'square'; side: number };
type Shape  = Circle | Square;

function area(shape: Shape): number {
  if (shape.kind === 'circle') {
    return Math.PI * shape.radius ** 2; // TS knows: Circle
  }
  return shape.side ** 2;               // TS knows: Square
}

Custom type guard function

typescript
interface ApiUser { id: number; name: string; email: string; }

function isApiUser(value: unknown): value is ApiUser {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id'    in value &&
    'name'  in value &&
    'email' in value
  );
}

const data: unknown = await response.json();
if (isApiUser(data)) {
  console.log(data.name); // safely typed as ApiUser
}

Exhaustiveness check with never

typescript
type Shape = Circle | Square | Triangle;

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':   return Math.PI * shape.radius ** 2;
    case 'square':   return shape.side ** 2;
    case 'triangle': return (shape.base * shape.height) / 2;
    default:
      const _exhaustive: never = shape; // Error if a new shape isn't handled
      throw new Error('Unhandled shape');
  }
}

Pro Tip

Use a discriminant field (`kind`, `type`, `__typename`) on union members - the literal type pattern is the most readable and efficient narrowing strategy. The `switch` on `kind` makes adding a new variant a compile error (via `never`) instead of a runtime bug.