Oliver Jumpertz

Posted on

Making good use of // @ts-expect-error in TypeScript

TypeScript is a a great language.
It solves a lot of JavaScript's scalability issues by adding types and also comes with a lot of other great features.
But it still compiles down to JavaScript and every package you provide for others to use (through npm, e.g.) should also be usable with JavaScript.

That is usually the case. But sometimes the usability of a package differs whether your users use it with JavaScript or TypeScript.
TypeScript users will have all features at hand you had when you implemented it.
JavaScript users will still have your type definitions (given you provided them) and can make use of them by using an extension (like VS Code has built-in), that enables in-editor type checking for JavaScript.

Let us assume that you care for your users and added type assertions to provide meaningful error messages at runtime.
No one has to look at the default stacktraces that can sometimes occur or has to deal with strange output no one would have expected.

The example package

The package you provide is a very simple, but important one. It comes with some handy functions for geometry calculations.
From calculating the area of a square to calculating the hypotenuse of a right triangle.
We will focus on the latter for our example.

The function calculateHypotenuse

This is the function to calculate the hypotenuse:

``````export default function calculateHypotenuse(a: number, b: number) {
assert(typeof a === 'number');
assert(typeof b === 'number');
return Math.sqrt(a * a + b * b);
}
``````

A quick dive into the Pythagorean Theorem

Given a right triangle, which is a triangle in which one of the angles is 90°, the Pythagorean theorem states that the area of the square formed by the longest side of the right triangle (the hypotenuse) is equal to the sum of the area of the squares formed by the other two sides of the right triangle.

Testing the function

Your most important unit test validates that your function is working as expected and returns the correct result:

``````describe('calculateHypotenuse', () => {
it('calculates the hypotenuse if provided with two valid arguments', () => {
const a = 1;
const b = 1;
const result = calculateHypotenuse(a, b);
expect(result).toEqual(1.4142135623730951);
});
});
``````

Great. You now have the most important line of your function covered.
They should be tested, too, because by doing this you ensure that you can change anything at any time, without having to worry that you break existing functionality.

You write another unit test, like this:

``````describe('calculateHypotenuse', () => {
it('calculates the hypotenuse if provided with two valid arguments', () => {
const a = 1;
const b = 1;
const result = calculateHypotenuse(a, b);
expect(result).toEqual(1.4142135623730951);
});

it('throws an AssertionError if a string is passed as first argument', () => {
const a = '1';
const b = 1;
expect(() => {
calculateHypotenuse(a, b);
}).toThrow();
});
});
``````

and let it run. But, oops. The compiler isn't happy!
It says:
`error TS2345: Argument of type '"1"' is not assignable to parameter of type 'number'.`

What now?

Well, this is where "// @ts-expect-error" becomes relevant.

// @ts-expect-error

// @ts-expect-error is a special comment that instructs the compiler to ignore any issues it finds when processing the next line.
You should not use it excessively only to suppress all warnings, and instead focus on solving the issue in another way.
But for our case it's the right choice.

Fixing the issue with your test

You can add the comment above the line that actually causes the compiler to complain, and everything now works!

Your complete tests now look like this:

``````describe('calculateHypotenuse', () => {
it('calculates the hypotenuse if provided with two valid arguments', () => {
const a = 1;
const b = 1;
const result = calculateHypotenuse(a, b);
expect(result).toEqual(1.4142135623730951);
});

it('throws an AssertionError if a string is passed as first argument', () => {
const a = '1';
const b = 1;

expect(() => {
// @ts-expect-error
calculateHypotenuse(a, b);
}).toThrow();
});
});
``````

You can now add another test for the second argument if you like, passing "b" as a string, and adding another comment above the function call, and you are done.

Why not // @ts-ignore

// @ts-ignore and // @ts-expect-error do the exact same thing. They suppress the error the compiler would emit at the line below the comment.
But // @ts-ignore is very generic. It just says: "Compiler, ignore the next line."
But what did you exactly want to suppress?
If you wanted to state why exactly that comment is there, it would cost you at least two lines of comments, like this:

``````// we want to ignore the error explicitly
// @ts-ignore
``````

// @ts-expect-error is more clear. It states the above in just one line.

Trying it out yourself

If you want to try the above out without setting up a project explicitly, you can clone or fork this repository on GitHub and play around yourself!

Want more?

If you liked this post, thank you very much for taking the time actually reading this.
Maybe you want to take a look at my Twitter or Instagram where I also post content like this in a shorter form.

I don't think you're pointing out the actual difference between `@ts-ignore` and `@ts-expect-error` in the Why not // @ts-ignore part.
They absolutely don't do the exact same thing, `@ts-expect-error` throws an error when there is no compilation error, so it needs an error. That's why you can test if you get a compilation error in a test by checking if `@ts-expect-error` throws an error for the next line. `@ts-ignore` just suppresses all errors, doesn't matter if there is one or not.