TypeScript Narrowing


TypeScript’s type system is powerful, but sometimes the compiler needs a little help understanding your intentions. This is where type narrowing comes in. Type narrowing allows you to refine the type of a variable within a specific scope, enabling more accurate type checking and preventing runtime errors.

Let’s say you have a variable that can be either a string or a number:

let value: string | number;

value = "hello";
value = 42;

If you try to access a string-specific method on value, TypeScript will complain, as it doesn’t know for certain that value is a string at that particular point:

console.log(value.toUpperCase()); // Error: Property 'toUpperCase' does not exist on type 'string | number'.

Here are several ways to narrow the type:

1. Type Guards:

Type guards are functions that return a boolean indicating whether a variable is of a specific type. The typeof and instanceof operators are common type guards.

function isString(val: any): val is string {
  return typeof val === "string";
}

if (isString(value)) {
  console.log(value.toUpperCase()); // No error, value is narrowed to string
}

2. Equality Narrowing:

Checking for equality with specific values can also narrow the type.

if (value === "hello") {
  console.log(value.toUpperCase()); // No error, value is narrowed to string
}

3. in operator Narrowing:

The in operator can check if a property exists on an object, effectively narrowing the type.

interface Dog {
  bark: () => void;
}

interface Cat {
  meow: () => void;
}

let animal: Dog | Cat;

if ("bark" in animal) {
  animal.bark(); // No error, animal is narrowed to Dog
} else {
  animal.meow(); // No error, animal is narrowed to Cat
}

4. Discriminated Unions:

Using a common property (a discriminant) in union types helps TypeScript narrow down the type based on the value of the discriminant.

interface Circle {
  kind: "circle";
  radius: number;
}

interface Square {
  kind: "square";
  sideLength: number;
}

type Shape = Circle | Square;

function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
  }
}

By employing these type narrowing techniques, you can write more robust and type-safe TypeScript code, leveraging the full potential of the type system. TypeScript will thank you for the extra guidance!