A new way to test types

Readers of Effective TypeScript and followers of this blog will know that testing types is a long-standing interest of mine:

  • typings-checker (2017) implemented $ExpectType and $ExpectError directives and helped to influence dtslint, which is used to test types on DefinitelyTyped.
  • I gave a talk at TSConf 2019 entitled Testing Types: An Introduction to dtslint.
  • I included Item 52: Be Aware of the Pitfalls of Testing Types in Effective TypeScript (2019)
  • I created literate-ts (2020) to type check Effective TypeScript and this blog.

There are many tools out there for testing types, from tricks with tsc to dtslint, tsd and literate-ts. But I can't say I really love any of them. I've always felt like I was writing tests because I should do it, rather than because it was fun and I wanted to.

When I started working on crudely-typed, I wondered whether there might be a better way. In the years since Effective TypeScript came out, largely thanks to Orta's advocacy, twoslash has become a widespread standard. You can see this on the TypeScript playground: if you write a twoslash comment (// ^?) then the TypeScript language service's "quick info" appears next to it:

This got me thinking: what if we used this same syntax to do type assertions?

So rather than dtslint's:

const x = nums.reduce((x, y) => x + y); // $ExpectType number

or literate-ts's:

const x = nums.reduce((x, y) => x + y);
// type is number

or tsd's:

const x = nums.reduce((x, y) => x + y);
expectType<number>(x);

you could just write a twoslash comment:

const x = nums.reduce((x, y) => x + y);
// ^? const x: number

And have something enforce that this comment matched the real Quick Info.

This has a few nice properties:

  1. It's a syntax that's already widely used.
  2. It's unambiguous which symbol the assertion refers to: it's the one one the caret (^) points at. (This is a source of ambiguity for dtslint and literate-ts.)
  3. It's clearly distinct from runtime code and is making an assertion about the display of the type, rather than its structure. (Structural checks will happily let you replace a nice-looking type with something cryptic but equivalent, or even with any.)

There was an existing eslint plugin, eslint-plugin-expect-type, which did something similar. So I set about adding support for twoslash syntax. One really nice thing came out of this: eslint makes it easy to write and test auto-fixers, so doing type assertions has some of the same feel as Jest's snapshot testing.

Here's a GIF of the autofixing in action:

Animation of eslint-plugin-expect-type filling in the correct type assertion

More than anything else, this autofixer is what's made writing type tests fun!

Here's an example of a type test from crudely-typed:

const typedDb = new TypedSQL(tables);
const docTable = typedDb.table('doc');
const update = docTable.update({where: ['title']});
// ^? const update: (db: Queryable, where: {
// title: string | null;
// }, update: Partial<Doc>) => Promise<Doc[]>
const newDoc = await update(
mockDb,
{title: 'Great Expectations'},
{created_by: 'Charles Dickens'},
);
newDoc;
// ^? const newDoc: Doc[]

crudely-typed uses many of the fancy types that I've written about on this blog. But users of the library should never be aware of any of this chicanery. The types that come out should make sense in the context of the types that go in. They shouldn't require you to understand the internals of the library. It's easy to accidentally break this property while refactoring, for example to make the type of newDoc display as something more complicated than Doc[] in the example above. Testing how types display gives you the freedom to refactor without the fear that you'll inadvertently worsen the experience of your library's users. And the autofixer makes it a delight to do so!

If you're writing a TypeScript library that makes use of any heavy type machinery, I'd highly recommend writing tests with eslint-plugin-expect-type. You're using eslint already (you are, aren't you?) so adding this plugin it doesn't require new tooling. You can see examples of how to wire it up on crudely-typed and crosswalk.

Like this post? Consider subscribing to my newsletter, the RSS feed, or following me on Twitter.
Effective TypeScript Book Cover

Effective TypeScript shows you not just how to use TypeScript but how to use it well. The book's 62 items help you build mental models of how TypeScript and its ecosystem work, make you aware of pitfalls and traps to avoid, and guide you toward using TypeScript’s many capabilities in the most effective ways possible. Regardless of your level of TypeScript experience, you can learn something from this book.

After reading Effective TypeScript, your relationship with the type system will be the most productive it's ever been! Learn more »