Carl Rippon

Building SPAs

Carl Rippon
BlogBooks / CoursesAbout
This site uses cookies. Click here to find out more

6 ways to narrow types in TypeScript

November 11, 2020
typescript

Type Narrowing

In a TypeScript program, a variable can move from a less precise type to a more precise type. This process is called type narrowing. We can use type narrowing to avoid type errors like the one below:

function addLeg(animal: Animal) {
  animal.legs = animal.legs + 1; // 💥 - Object is possibly 'undefined'
}

In this post, we are going to cover 6 different ways we can narrow types in TypeScript.

Using a conditional value check

The Animal type in the above example is as follows:

type Animal = {
  name: string;
  legs?: number;
};

TypeScript raises a type error because the legs property in the addLeg function because it could be undefined, and it doesn’t make sense to add 1 to undefined.

A solution is to check whether legs is truthy before it is incremented:

function addLeg(animal: Animal) {
  if (animal.legs) {    animal.legs = animal.legs + 1;
  }}

legs is of type number | undefined before the if statement and is narrowed to number within the if statement. This type narrowing resolves the type error.

This approach is useful for removing null or undefined from types or removing literals from union types.

Using a typeof type guard

Consider the following function which duplicates the parameter if it is a string or doubles it if it is a number:

function double(item: string | number) {
  if (typeof item === "string") {
    return item.concat(item); // item is of type string
  } else {
    return item + item; // item is of type number
  }
}

The item parameter is of type number or string before the if statement. Within the if branch, item is narrowed to string, and within the else branch, item is narrowed to number.

This pattern is called a typeof type guard and is useful for narrowing union types of primitive types.

Using an instanceof type guard

Lots of the types we use are more complex than primitive types though. Consider the following types:

class Person {
  constructor(
    public firstName: string,
    public surname: string
  ) {}
}
class Organisation {
  constructor(public name: string) {}
}
type Contact = Person | Organisation;

The following function raises a type error:

function sayHello(contact: Contact) {
  console.log("Hello " + contact.firstName);
  // 💥 - Property 'firstName' does not exist on type 'Contact'.
}

This is because contact might be of type Organisation, which doesn’t have a firstName property.

An instanceof type guard can be used with class types as follows:

function sayHello(contact: Contact) {
  if (contact instanceof Person) {    console.log("Hello " + contact.firstName);
  }}

The type of contact is narrowed to Person within the if statement, which resolves the type error.

Using an in type guard

We don’t always use classes to represent types though. The types in the last example could be as follows:

interface Person {
  firstName: string;
  surname: string;
}
interface Organisation {
  name: string;
}
type Contact = Person | Organisation;

The instanceof type guard doesn’t work with the interfaces or type aliases. Instead we can use an in operator type guard:

function sayHello(contact: Contact) {
  if ("firstName" in contact) {    console.log("Hello " + contact.firstName);
  }}

The type of contact is narrowed to Person within the if statement, which means no type error occurs.

Using a type guard function with a type predicate

Consider the following example:

type Rating = 1 | 2 | 3 | 4 | 5;

async function getRating(productId: string) {
  const response = await fetch(
    `/products/${productId}`
  );
  const product = await response.json();
  const rating = product.rating;
  return rating;
}

The return type of the function is inferred to be Promise<any>. So, we if assign a variable to the result of a call to this function, it will have the any type:

const rating = await getRating("1"); // type of rating is `any`

This means that no type checking will occur on this variable. 😞

We can use a type guard function that has a type predicate to make this code more type-safe. Here is the type guard function:

function isValidRating(
  rating: any
): rating is Rating {
  if (!rating || typeof rating !== "number") {
    return false;
  }
  return (
    rating === 1 ||
    rating === 2 ||
    rating === 3 ||
    rating === 4 ||
    rating === 5
  );
}

rating is Rating is the type predicate in the above function.

A type guard function must return a boolean value if a type predicate is used.

We can use this type guard function in our example code as follows:

async function getRating(productId: string) {
  const response = await fetch(
    `/products/${productId}`
  );
  const product = await response.json();  const rating = product.rating;
  if (isValidRating(rating)) {
    return rating; // type of rating is `Rating`
  } else {
    return undefined;
  }
}

The type of rating inside the if statement is now Rating.

Nice! 😀

Using a type guard function with an assertion signature

Continuing on with the rating example, we may want to structure our code a little differently:

async function getRating(productId: string) {
  const response = await fetch(
    `/products/${productId}`
  );
  const product = await response.json();  const rating = product.rating;
  checkValidRating(rating); // should throw error if invalid rating
  return rating; // type should be narrowed to `Rating`
}

Here we want the checkValidRating function to throw an error if the rating is invalid.

We also want the type of rating to be narrowed to Rating after checkValidRating has been successfully executed.

We can use a type guard function with an assertion signature to do this:

function checkValidRating(
  rating: any
): asserts rating is Rating {
  if (!rating || typeof rating !== "number") {
    throw new Error("Not a rating");
  }
  if (
    rating !== 1 &&
    rating !== 2 &&
    rating !== 3 &&
    rating !== 4 &&
    rating !== 5
  ) {
    throw new Error("Not a rating");
  }
}

asserts rating is Rating is the assertion signature in the above type guard function. If the function returns without an error being raised, then the rating parameter is asserted to be of type Rating.

In getRating, the rating variable is of type Rating after the call to checkValidRating. 😀

Wrap up

TypeScript automatically narrows the type of a variable in conditional branches. Doing a truthly condition check will remove null and undefined from a type. A typeof type guard is a great way to narrow a union of primitive types. The instanceof type guard is useful for narrowing class types. The in type guard is an excellent way of narrowing object types. Function type guards are helpful in more complex scenarios where the variable’s value needs to be checked to establish its type.

Did you find this post useful?

Let me know by sharing it on Twitter.
Click here to share this post on Twitter

If you to learn more about TypeScript, you may find my free TypeScript course useful:

Take a look