Does something look odd about this unit test?
it('should square 4', () => {square(4);});
Sure. It’s not asserting anything! It doesn’t matter whether square
is implemented correctly. So long as the function doesn't throw an exception, this test will pass.
This isn’t great. The test would be much better if it checked the return value of square(4)
:
it('should square 4' () => {expect(square(4)).to.equal(16);});
Crazy as the first example is, it’s exactly how the type declarations in DefinitelyTyped have traditionally been tested. It didn’t matter what the types were, so long as the type checker didn’t find any errors. Particularly in the presence of [any](https://www.typescriptlang.org/docs/handbook/basic-types.html#any)
types, this makes for some weak tests. Weak tests lead to imprecise and inaccurate typings, and they make refactoring type declarations scary.
Microsoft recently introduced a new tool, dtslint, which makes assertions in type declaration tests possible. The rest of this post explains how to use it to bring all the benefits of testing to type declaration files.
Here are a few lines from the underscore tests for _.pluck
, which I've written about before:
Note the lack of assertions on the return type. What this is really checking is that there is a function named _.pluck
and that it accepts a list and a string as parameters.
The return type should be string[]
, but it's any[]
. Too bad! How can we make the test fail?
dtslint
to the rescue! To check the return type of the call to _.pluck
, we can use an // $ExpectType
assertion:
When we run tsc
on this test it passes. But when we run dtslint
on it we get the following:
ERROR: 2:1 expect Expected type to be: string[]got: any[]
Tada! Caught!
We can make the declaration precise using a mapped type:
Now we get the following output from dtslint
:
Test with 2.8Test with 2.7Test with 2.6Test with 2.5Test with 2.4Test with 2.3Test with 2.2Test with 2.1Test with 2.0Error: /Users/danvk/github/dtslint-post/types/index.d.ts:1:33ERROR: 1:33 expect Compile error in [email protected] but not in [email protected] with a comment '// TypeScript Version: 2.1' just under the header.Cannot find name 'keyof'.
The tests pass with TypeScript 2.1+, but not with TypeScript 2.0. This make sense since keyof
was introduced in TypeScript 2.1. Before TS 2.1, it wasn't possible to type pluck
this precisely. So our only real option is to require a newer version using the suggested comment:
This gets at another reason that type declarations are hard to maintain. There are actually three independent versions involved in type declarations:
FlowTyped chooses to explicitly model this, whereas DefinitelyTyped does not.
Suppose we’re working with type declarations for lodash’s [map](https://lodash.com/docs#map)
function:
export function map<U, V>(array: U[], fn: (u: U) => V): V[];
You use this much like Array.prototype.map
:
_.map([1, 2, 3], x => x * x); // returns [1, 4, 9].
Lodash has no _.pluck
function. Instead, it adds a variant of _.map
:
We’d to model this in the type declarations, but it’s scary to alter the type of such an important function! This is one of the very best reasons to write tests: they let you refactor with confidence. dtslint
lets you do the same with type declarations.
Here’s a dtslint test for _.map
that covers both the old and new declarations:
Now we can add an overload to the declaration for map
:
When dtslint
passes, we can be confident that we've both added the new functionality and avoided changing existing behavior.
Callbacks are pervasive in JavaScript and it’s important that type declarations accurately model their parameters. dtslint
can help here, too: if we're careful about formatting, we can make assertions about the types of callback parameters.
_.map
actually passes three parameters to its callback. This snippet tests that all of them have the correct types inferred:
If we change any of those $ExpectType
lines, we'll get an error. (This is often a good sanity check!)
It’s famously hard to know what this
refers to in JavaScript. But TypeScript can help! If a library manipulates this
in its callbacks, then the type declarations should model that.
If you’ve made it this far, you won’t be surprised to find out that dtslint
can help here, too! Just write a type assertion for this
:
Dealing with inaccurate or imprecise type declarations can be one of the most frustrating aspects of working in TypeScript. They can introduce false errors or give you an unwarranted sense of confidence by introducing any
types where you weren't expecting them.
Testing is the key to improving an existing code base, and dtslint
brings many of these benefits to TypeScript's type language. It lets you pin down existing behavior so that you can refactor type declarations with confidence. It even lets you do test-driven development with type declaration files!
dtslint
is already in use in the DefinitelyTyped repo today. So if you're writing type declarations, please write some type assertions! And if you're changing existing type declarations, please write assertions for the existing behavior.
It's my hope that, over the long run, dtslint
will lead to dramatically higher quality type declarations for all TypeScript users. And that means a better TypeScript experience, even if you don't know that dtslint
exists!
Check out this repo to see all the code samples from this post in action. If you’d like to use dtslint outside of DefinitelyTyped, check out Paul Körbitz’s great post.