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:
|
or literate-ts's:
|
or tsd's:
|
you could just write a twoslash comment:
|
And have something enforce that this comment matched the real Quick Info.
This has a few nice properties:
- It's a syntax that's already widely used.
- 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.) - 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:
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:
|
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.