TypeScript Type Narrowing: Writing Safer Code with Precision

When working with TypeScript, you often deal with union types, meaning a variable can have multiple possible types. But how do you safely determine the actual type at runtime? This is where type narrowing comes in.

TypeScript narrowing means refining a variable’s type within a certain scope based on runtime checks. This allows the TypeScript compiler to provide better type inference and prevent unnecessary type errors.


🔍 Why Type Narrowing Matters

Consider this simple function:

function printLength(value: string | string[]) {
  return value.length;
}

Here, value could be either a string or an array of strings. But TypeScript will complain because .length is valid for both types, yet they behave differently (a string’s .length is a character count, while an array’s .length counts items).

We can fix this with narrowing.


🔹 1. Using typeof for Primitive Types

The typeof operator helps us narrow down primitives like string, number, and boolean:

function printLength(value: string | string[]) {
  if (typeof value === "string") {
    console.log(`String length: ${value.length}`);
  } else {
    console.log(`Array length: ${value.length}`);
  }
}

Now TypeScript knows that inside if (typeof value === "string"), value must be a string. Otherwise, it must be an array.


🔹 2. Using instanceof for Class-Based Types

When dealing with class instances, instanceof helps TypeScript determine an object’s type:

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();
  }
}

Here, instanceof ensures TypeScript treats animal as a Dog inside the if block and as a Cat otherwise.


🔹 3. Creating Custom Type Guards

For more complex cases, TypeScript allows custom type guards. These are functions that return a boolean while asserting the type:

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

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

function move(pet: Fish | Bird) {
  if (isFish(pet)) {
    pet.swim(); // TypeScript now knows pet is a Fish
  } else {
    pet.fly();
  }
}

The function isFish checks whether swim exists, allowing TypeScript to infer that pet is a Fish.


✅ Best Practices for Type Narrowing

  1. Always prefer typeof and instanceof when dealing with primitive or class-based types.
  2. Use custom type guards for more complex scenarios.
  3. Leverage discriminated unions (objects with a common type field) for better readability.

🚀 Final Thoughts

Type narrowing is an essential TypeScript skill that makes your code safer and more predictable. Instead of making risky type assertions (as any), you can let TypeScript do the work for you by refining types intelligently.

Next time you work with union types, think about narrowing—your future self (and your TypeScript compiler) will thank you! operator helps us narrow down primitives like string, number, and boolean: