TYPESCRIPT

TypeScript Type Narrowing: Syntax, Usage, and Examples

TypeScript type narrowing allows you to make your code smarter by reducing a broad type to a more specific one within a certain scope. This improves safety and helps TypeScript give you better autocompletion, error-checking, and code suggestions.

How to Use TypeScript Type Narrowing

You apply type narrowing TypeScript techniques by using conditional logic to check a variable's type, then let TypeScript infer a more specific type based on your condition.

Here’s a basic example:

function printValue(val: string | number) {
  if (typeof val === "string") {
    console.log(val.toUpperCase()); // val is narrowed to string
  } else {
    console.log(val.toFixed(2)); // val is narrowed to number
  }
}

In this example, the union type string | number is narrowed using a typeof check. TypeScript uses this information to treat the variable as a string or number inside the respective blocks.

When to Use TypeScript Type Narrowing

You should use type narrowing in TypeScript when:

  • You’re working with union types.
  • A variable might be null or undefined and needs a check.
  • You want to write safer, more predictable conditional logic.
  • Your function can accept different data shapes, and you want to handle each one properly.

Narrowing types reduces the risk of runtime errors and makes your code easier to read and maintain.

Examples of Type Narrowing TypeScript Style

Narrowing with typeof

You can narrow primitive types with typeof.

function handleInput(input: string | number) {
  if (typeof input === "string") {
    console.log("String length:", input.length);
  } else {
    console.log("Number squared:", input * input);
  }
}

TypeScript knows exactly what type you're working with in each block.

Narrowing with instanceof

Use instanceof to check if an object was created with a particular class.

class Dog {
  bark() {
    console.log("Woof!");
  }
}

class Cat {
  meow() {
    console.log("Meow!");
  }
}

function makeSound(animal: Dog | Cat) {
  if (animal instanceof Dog) {
    animal.bark();
  } else {
    animal.meow();
  }
}

This technique helps TypeScript narrow the type to the correct class.

Narrowing with Discriminated Unions

A discriminated union uses a shared literal property to differentiate object types.

type Square = { kind: "square"; size: number };
type Circle = { kind: "circle"; radius: number };
type Shape = Square | Circle;

function area(shape: Shape) {
  if (shape.kind === "square") {
    return shape.size ** 2;
  } else {
    return Math.PI * shape.radius ** 2;
  }
}

By checking the kind field, you narrow the type to Square or Circle.

Narrowing with in Operator

The in operator checks for property existence and narrows accordingly.

type Car = { make: string; model: string };
type Bike = { brand: string; gearCount: number };

function describeVehicle(vehicle: Car | Bike) {
  if ("make" in vehicle) {
    console.log(`Car: ${vehicle.make} ${vehicle.model}`);
  } else {
    console.log(`Bike: ${vehicle.brand} with ${vehicle.gearCount} gears`);
  }
}

This technique is useful for distinguishing between object types that don’t share a discriminant property.

Narrowing with Null Checks

You can use null or undefined checks to narrow types that include those values.

function greet(user: string | null) {
  if (user !== null) {
    console.log(`Hello, ${user}`);
  } else {
    console.log("No user provided");
  }
}

This prevents calling string methods on null.

Learn More About Type Narrowing TypeScript Techniques

Custom Type Guards

A custom type guard is a function that helps you narrow a type using a return signature.

type Fish = { swim: () => void };
type Bird = { fly: () => void };

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

function move(animal: Fish | Bird) {
  if (isFish(animal)) {
    animal.swim();
  } else {
    animal.fly();
  }
}

This gives you full control over narrowing logic and makes the code reusable.

Type Predicates

A type predicate has the form parameterName is Type, and it’s what lets TypeScript apply your custom narrowing logic.

function isString(value: unknown): value is string {
  return typeof value === "string";
}

Once TypeScript sees the is keyword, it knows how to refine the type based on your check.

Exhaustiveness Checks

When you use discriminated unions, it’s smart to handle all possible types. The never type helps catch unhandled cases.

function getShapeName(shape: Shape): string {
  switch (shape.kind) {
    case "square":
      return "Square";
    case "circle":
      return "Circle";
    default:
      const _exhaustive: never = shape;
      return _exhaustive;
  }
}

If you later add a Triangle type to your Shape union, TypeScript will throw an error until you update this function.

Narrowing Inside Loops

TypeScript also applies narrowing within loops:

function logAll(values: (string | number)[]) {
  for (const val of values) {
    if (typeof val === "string") {
      console.log("Uppercased:", val.toUpperCase());
    } else {
      console.log("Fixed:", val.toFixed(2));
    }
  }
}

You don't need to cast or check outside the loop—TypeScript narrows inside the block.

Combining Multiple Narrowing Techniques

You can combine typeof, in, instanceof, and custom type guards in complex scenarios.

function process(value: string | number | Date) {
  if (typeof value === "string") {
    console.log(value.toUpperCase());
  } else if (typeof value === "number") {
    console.log(value.toFixed(1));
  } else if (value instanceof Date) {
    console.log(value.toISOString());
  }
}

By chaining checks, you give TypeScript all the info it needs to narrow precisely.

Best Practices for TypeScript Type Narrowing

  • Keep type checks simple and obvious. Avoid overcomplicated logic that might confuse TypeScript.
  • Use discriminated unions wherever possible—they make narrowing much more reliable.
  • Write custom type guards for reusable, readable logic.
  • Use exhaustiveness checks with never to avoid missing any types when handling unions.
  • Prefer typeof and instanceof for primitives and class instances.
  • Avoid overusing type assertions. If you're casting too often, consider whether better narrowing could solve the issue.

Why TypeScript Type Narrowing Improves Your Code

TypeScript type narrowing helps you:

  • Catch bugs before they happen.
  • Avoid invalid operations (e.g., calling .toUpperCase() on number).
  • Improve code readability and maintainability.
  • Let TypeScript assist with better autocomplete and hints.
  • Write fewer type assertions or manual casts.

When you write a function that accepts broad types and need to safely operate on more specific ones, narrowing is the way to go.

TypeScript type narrowing turns your broad, flexible types into focused, actionable logic. You guide the compiler using type checks, and TypeScript rewards you with smarter inference and safer code. Whether you’re working with unions, optional values, or third-party data, narrowing gives you the confidence that your code handles each case exactly as intended.

Learn TypeScript for Free
Start learning now
button icon
To advance beyond this tutorial and learn TypeScript by doing, try the interactive experience of Mimo. Whether you're starting from scratch or brushing up your coding skills, Mimo helps you take your coding journey above and beyond.

Sign up or download Mimo from the App Store or Google Play to enhance your programming skills and prepare for a career in tech.

You can code, too.

© 2025 Mimo GmbH