Code Review Videos > JavaScript / TypeScript / NodeJS > TypeScript Type Guard Example

TypeScript Type Guard Example

I was reviewing some TypeScript code this afternoon that had a type assertion (as) (also commonly called a ‘cast’) in there that could have been avoided by using a Type Guard.

Type Guards are one of my favourite features of TypeScript. That makes me sound like a real nerd. But hey, I might convince you along the way. You never know. Then we can be bff nerd friends 🤓🤓

Here’s an example of the code I saw. It’s not the exact code, because that stuff is sadly confidential, so I have to make approximations:

type Success = { response: number; body: string };
type Failure = { error: true; message: number };

const handler = (response: Success | Failure) => {
  if ((response as Failure).error) {
    // Handle Failure type
    throw new Error("Boom!");
  } else {
    // Handle Success type
    console.log("success!");
  }
};Code language: TypeScript (typescript)

This code gives us two different types of responses. We either get a Success which has one shape, or a Failure, which has another.

There’s a handler function which will receive the response, and at the point it receives it, we do not know if the response is good or bad.

If the response is bad, we need to do one thing. And if it is good, we need to do something else.

The simplest approach won’t work:

TS2339: Property error does not exist on type Success | Failure
Property error does not exist on type Success

If we try to access the error property on the response object, TypeScript is saying hey, error is a valid property of a Failure response, but it isn’t present on a response of type Success.

There are probably more than two ways to resolve this issue.

One is with casting, like we saw above, and like was used in the PR I was reviewing.

Assume we use the code snippet from the start of this post, and the following tests. This works absolutely fine:

import handler, { Failure, Success } from "./index";

describe("handler function", () => {
  it("should throw an error for Failure type", () => {
    // Arrange
    const failureResponse: Failure = { error: true, message: 42 };

    // Act and Assert
    expect(() => handler(failureResponse)).toThrowError("Boom!");
  });

  it('should log "success!" for Success type', () => {
    // Arrange
    const successResponse: Success = { response: 200, body: "OK" };

    // Act
    const consoleSpy = jest.spyOn(console, "log");
    handler(successResponse);

    // Assert
    expect(consoleSpy).toHaveBeenCalledWith("success!");
  });
});Code language: TypeScript (typescript)

Giving:

typescript cast test passing

Sometimes a cast is the easiest solution to a problem. If it’s a trivial problem, that’s probably fine. It all comes down to circumstances.

Why I wasn’t so keen on this one was that the same casting was used in multiple places across multiple files.

Better solutions to this problem exist.

An Inline Type Guard Solution

Taking that initial snippet once more:

type Success = { response: number; body: string };
type Failure = { error: true; message: number };

const handler = (response: Success | Failure) => {
  if ((response as Failure).error) {
    throw new Error("Boom!");
  } else {
    console.log("success!");
  }
};
Code language: TypeScript (typescript)

We can change this to use a type guard directly in line:

const handler = (response: Success | Failure) => {
  if ('error' in response) {
    throw new Error("Boom!");
  } else {
    console.log("success!");
  }
};
Code language: JavaScript (javascript)

By using in we can check if a certain property exists in an object, and based on that, TypeScript narrows down the type of the variable.

You can do this natively in JavaScript, too. In fact if you compile this TypeScript, you get the equivalent code output as JavaScript:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var handler = function (response) {
    if ("error" in response) {
        // Handle Failure type
        throw new Error("Boom!");
    }
    else {
        // Handle Success type
        console.log("success!");
    }
};
exports.default = handler;Code language: JavaScript (javascript)

I guess if you’re really old school, you could also have gone with response.hasOwnProperty('error') to achieve a similar check.

Using in like this is pretty useful for a one off type narrowing.

But in the PR I was reviewing, there were several instances. So a better solution might be to extract this out to a Type Guard function.

A Type Guard Function

Rather than inline our type guard logic, we can extract the code out to a standalone, and therefore re-usable function.

This is easier for me to show than explain without code, so here goes:

export type Success = { response: number; body: string };
export type Failure = { error: true; message: number };

const isFailure = (response: Success | Failure): response is Failure => {
  return "error" in response;
};

const handler = (response: Success | Failure) => {
  if (isFailure(response)) {
    throw new Error("Boom!");
  } else {
    console.log("success!");
  }
};

export default handler;
Code language: TypeScript (typescript)

Personally I think this is really cool.

isFailure takes an object that could be one of two different types.

By process of elimination, the result will be of only one known type.

In other words, if isFailure returns true, the given response object must have been of type Failure.

This example is trivialised, but if your type checking is a little more involved, perhaps like:

const isHighSeverityFailure = (response: Success | Failure, threshold: number): response is Failure => {
  return 'error' in response && response.message > threshold;
};Code language: TypeScript (typescript)

Then you can probably start to see that repeating that in multiple places would be a nightmare, so having a single function you can make use off whenever dealing with a response is a pretty handy thing indeed.

Anyway, just my thoughts.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.