When it comes to TypeScript code:
In this blog post, we look at the latter.
Consider a utility type such a the built-in Pick<Type, Keys>
:
interface Person {
first: string;
last: string;
}
type Friend = Pick<Person, 'first'>;
// type Friend = {
// first: string
// }
The three lines at the end serve two purposes:
Pick
works.Friend
is as intended (think manual unit test for types).In both cases, we’d profit from an automated check:
Type testing can also help us when JavaScript constructs such as functions have complicated types.
We can test a type by checking if a value is assignable to it (line A) or via the satisfies
operator (line B):
interface Person {
first: string;
last: string;
}
type Friend = Pick<Person, 'first'>;
const friend1: Friend = { // (A)
first: 'Robin',
};
({
first: 'Flo',
}) satisfies Friend; // (B)
However, the previous tests only check that Friend
doesn’t have more properties than the ones we are using. It doesn’t prevent Friend
from having fewer properties. For example, TypeScript is fine with the following code:
type Friend = {};
const friend1: Friend = {
first: 'Robin',
};
({
first: 'Flo',
}) satisfies Friend;
Some type testing libraries implement type checks in TypeScript and lets us use them in our code.
tsafe
The library tsafe
lets us check types via the type parameter of a function assert()
(line A):
import { assert } from 'tsafe/assert';
import type { Equals } from 'tsafe';
interface Person {
first: string;
last: string;
}
assert<Equals< // (A)
Pick<Person, 'first'>,
{ first: string }
>>();
@esfx/type-model/test
Package @esfx/type-model
has a module test
with the generic type Test
for type checks:
import { Test, ExpectType } from '@esfx/type-model/test';
// test suite
type _ = [
Test<ExpectType<
Pick<Person, 'first'>,
{ first: string }
>>,
];
tsd
tsd
is a tool for running tests against .d.ts
files. It looks similar to the previous two approaches, but performs custom compilation to check its type assertions.
concat.d.ts
:
declare const concat: {
(value1: string, value2: string): string;
(value1: number, value2: number): number;
};
export default concat;
concat.test-d.ts
import {expectType} from 'tsd';
import concat from './concat.js';
expectType<string>(concat('foo', 'bar'));
expectType<number>(concat(1, 2));
If you want to implement an equality check for types yourself, there is a StackOverflow question whose answers have useful information.
Another approach for testing types is to write expected types in comments and use a tool to compare those types against actual types.
dtslint
dtslint
is a tool that is part of the DefinitelyTyped-tools. Using it looks like this:
function myFunc(n: number): void {}
// $ExpectType void
myFunc(123);
eslint-plugin-expect-type
Using the ESLint plugin eslint-plugin-expect-type
looks similar to dtslint
:
function myFunc(n: number): void {}
// $ExpectType void
myFunc(123);
This plugin also supports twoslash syntax (^?
):
//===== Single-line annotations =====
const square = (x: number) => x * x;
const four = square(2);
// ^? const four: number
//===== Multi-line annotations =====
const vector = {
x: 3,
y: 4,
};
vector;
// ^? const vector: {
// x: number;
// y: number;
// }
Sometimes we want to check that the wrong input produces errors. In JavaScript unit testing that is done (e.g.) via assert.throws()
. This section examines the equivalents for type testing.
tsd
With tsd
, we check for errors via the function expectError()
.
misc.d.ts
:
export declare class MyClass {}
misc.test-d.ts
import {expectError} from 'tsd';
import {MyClass} from './misc.js';
expectError(MyClass());
// We forgot `new`:
// “Value of type 'typeof MyClass' is not callable.”
This approach has two downsides:
dtslint
Checking for errors with dtslint
looks like this:
function myFunc(n: number): void {}
// @ts-expect-error
myFunc('abc');
Using @ts-expect-error
has the benefit that TypeScript won’t complain at compile time. Alas, we still don’t see the error message.
Error checking with eslint-plugin-expect-type:
function myFunc(n: number): void {}
// $ExpectError
myFunc('abc');
The downsides are: TypeScript complains at compile time and we don’t see the error message.
@ts-expect-error
For my book, “Tackling TypeScript”, I have written a simple type error checking tool (which, alas, is not ready for public consumption at the moment). It checks if the error message after @ts-expect-error
matches what TypeScript would report without the annotation. That looks as follows (source of this example):
class Color {
name: string;
private branded = true;
constructor(name: string) {
this.name = name;
}
}
class Person {
name: string;
private branded = true;
constructor(name: string) {
this.name = name;
}
}
const person: Person = new Person('Jane');
// @ts-expect-error: Type 'Person' is not assignable to type
// 'Color'. Types have separate declarations of a private
// property 'branded'. (2322)
const color: Color = person;
This time, TypeScript won’t complain and we see the error message.
If anyone is interested: I have created a Gist with all examples in “Tackling TypeScript” where I use this kind of error checking.
My impression so far:
Code tests work better for testing code:
Comment tests work better for testing examples (e.g. embedded in Markdown):
Thanks for all the replies in this Mastodon thread! They provided crucial information for this blog post.