DEV Community 👩‍💻👨‍💻

Cover image for Custom Type Guards in Typescript
Maina Wycliffe for This is Learning

Posted on • Originally published at mainawycliffe.dev

Custom Type Guards in Typescript

Previously, we covered various approaches that you can take to narrowing types in Typescript. Type narrowing is the process of moving the type of a variable from a less precise to a more precise type i.e. from a union of string and number to string only. You can learn more about type narrowing here.

In this article, we are going to look at how we can create our own custom type guards. Custom type guards will help you to check if a variable is of a certain type before usage, which helps in Type narrowing.

Take for instance the following function, which calculates the area of a shape i.e. Circle or Rectangle.

function calculateArea(shape: Rectangle | Circle) {
    // calculate area
}
Enter fullscreen mode Exit fullscreen mode

In order to calculate the area, we will need to determine whether the shape being passed in is a Rectangle or Circle. We can create a custom type guide that will check if the type of a Rectangle and calculate its area, otherwise calculates the area of a circle:

if(isRectangle(shape)) {
    // calculate area of a rectangle
} else {
    // calculate area of a circle
}
Enter fullscreen mode Exit fullscreen mode

What is a Type Predicate?

A type predicate is a function return type that tells typescript a parameter is of a specific type. A predicate takes the following format: parameterName is Type, where parameterName must be the name of a parameter in the function parameter signature.

For instance, if we wanted to build the custom type guard isRectangle above, our type predicate would be shape is Rectangle, where shape is the parameter name, as shown below.

function isRectangle(shape: unknown): shape is Rectangle {
    // function body
}
Enter fullscreen mode Exit fullscreen mode

Custom Type Guard

To define a custom type guard, we create a function that returns a type predicate. The function itself just needs to return true or false. If we take the example above for isRectangle type guard, we would check if the width and the height are present and return true, otherwise, return false.

function isRectangle(shape: unknown): shape is Rectangle {
  if ("width" in shape && "height" in shape) {
    // this is a rectangle
    return true; 
  }
  // it's not a rectangle
  return false;
}
Enter fullscreen mode Exit fullscreen mode

In the above example, we are using Javascripts in operator to check if the width and height properties are in the shape object.

Usage

To use the custom type guard, we use it just like any other function that returns a boolean.

type Rectangle = {
  height: number;
  width: number;
}

type Circle = {
  radius: number;
}

const r: Rectangle = {
  height: 12,
  width: 15
}

const c: Circle = {
  radius: 10,
}

console.log(isReactangle(r)); // true
console.log(isReactangle(c)) // false
Enter fullscreen mode Exit fullscreen mode

By using it within a control flow, you can narrow the type of the variable, just like other methods of narrowing types.

function area(shape: Rectangle | Circle) {
  if(isRectangle(shape)) {
    // Rectangle
    shape.height // no error
    shape.radius // error
  } else {
    // Circle
    shape.radius // no error
    shape.height // error
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this brief article, we learned what a Type predicate is and how to build custom type guards. We learned that a type guard is a special function that returns a type predicate so that typescript is able to determine the type of a variable.

We will continue covering similar topics in Typescript in this series - A Byte of Typescript. A Byte of Typescript is a new series that I will be publishing on a regular basis to help you demystify Typescript.

If you are looking to learn more about Typescript, here are the previous articles I have published. Thank you 😄.

Top comments (9)

Collapse
lukeshiru profile image
Luke Shiru • Edited on

One thing worth mentioning is that your function can return the logic directly without the need of an if, and instead of unknown, you could use Record<PropertyKey, unknown> instead if you're expecting an object (ideally you should also test the type of those properties):

const isRectangle = (shape: Record<PropertyKey, unknown>): shape is Rectangle =>
    "width" in shape &&
    typeof (shape as Rectangle).width === "number" &&
    "height" in shape &&
    typeof (shape as Rectangle).height === "number";
Enter fullscreen mode Exit fullscreen mode

Or if you want to test unknown, then you need to check that is an actual object and is not null first:

const isRectangle = (shape: unknown): shape is Rectangle =>
    typeof shape === "object" &&
    shape !== null &&
    "width" in shape &&
    typeof (shape as Rectangle).width === "number" &&
    "height" in shape &&
    typeof (shape as Rectangle).height === "number";
Enter fullscreen mode Exit fullscreen mode

Cheers!

Collapse
mainawycliffe profile image
Maina Wycliffe Author

Yeah, it can but I find this shorthands to be unreadable especially for the context of a tuturial, So I chose to be as verbose as possible.

Collapse
lukeshiru profile image
Luke Shiru

The main concern is still that the guard would give false positives (something that has, for example, the same properties but with type string instead of number). You need to take a Record or if you take unknown you need to check if is an object first.

Thread Thread
mainawycliffe profile image
Maina Wycliffe Author

Yeah, that's true. I probably should add that you need to do an exhaustive check on the type before returning it.

Collapse
lioness100 profile image
Lioness100

Do you know what the difference between shape is Rectangle and asserts shape is Rectangle is?

Collapse
mainawycliffe profile image
Maina Wycliffe Author

I don't understand, how is the second one used?

Collapse
lioness100 profile image
Lioness100

I think I understand it. If a return type is described as x is y, then typescript assumes that if the function returns true x is y.

declare const x: y | z;
if (isY(x)) {
  // `x` is `y`;
}

// `x` is `y | z`
Enter fullscreen mode Exit fullscreen mode

However, with asserts x is y, typescript will assume that the function will throw if x isn't y.

declare const x: y | z;

// will throw if `x` isn't `y`
assertIsY(x);

// `x` is `y`
Enter fullscreen mode Exit fullscreen mode

Super cool!

Thread Thread
mainawycliffe profile image
Maina Wycliffe Author

Yeah, that is true, it's upto you to do an exhaustive check on whether an object is of that type before returning true.

Thread Thread
lioness100 profile image
Lioness100

Yeah, I understood the concept, I was just confused by the difference of asserts x is y vs x is y

🌚 Friends don't let friends browse without dark mode.

Good news! You can update to dark mode in your DEV settings.