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:
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:
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.