To make the construction and maintenance of more advanced types easier it can be helpful to write some tests that ensure their correct function. This sounds a little easier than it turns out to be.
As part of the ecosystem for TypeScript Microsoft have written and released the dtslint
tool. It can be used to link and compile TypeScript types for static analysis and mostly serves to
keep the @types/*
packages in line.
Firstly, install the dependencies that we will need to test the types.
npm install --save-dev dtslint conditional-type-checks
Then in the directory you wish to write your tests (the examples in this article use a directory
./typings/__tests__
for this) - create a new file index.d.ts
with the following contents:
// TypeScript Version: 3.3
// see https://github.com/Microsoft/dtslint#specify-a-typescript-version for more information
The first line lets dtslint know which TypeScript version you would like to test your types against.
In that same directory you will also need to include a tsconfig.json
file like the following:
// this additional tsconfig is required by dtslint
// see: https://github.com/Microsoft/dtslint#typestsconfigjson
{
"compilerOptions": {
"module": "commonjs",
"lib": ["es6"],
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noEmit": true,
// If the library is an external module (uses `export`), this allows your test file to import "mylib" instead of "./index".
// If the library is global (cannot be imported via `import` or `require`), leave this out.
"baseUrl": "."
}
}
Finally, dtslint
needs to be added to the configuration for tslint in the tslint.json
:
{
"extends": ["dtslint/dtslint.json"],
"rules": {
"no-useless-files": false,
"no-relative-import-in-test": false
}
}
You should now have a directory structure that looks something like the following:
typings
`-- __tests__
`-- index.d.ts
`-- tsconfig.json
`-- tslint.json
Now in your package.json file you can add a script to run the dtslint
testing.
"ts:dtslint": "dtslint ./typings/__tests__",
To make the next few steps easier to follow we’ll quickly write out a new TypeScript type
in ./typings
- without this we wouldn’t actually have anything to test! So, let’s write
an implementation of the Omit
type that now comes with the TypeScript standard library.
It uses both Pick
and Exclude
, which are also included with TypeScript. If they are new
to you then you might like to read my previous article
Advanced TypeScript Types to get an introduction first.
/**
* Remove all keys listed in K from the object T
*
* @example type MyType = Omit<{ a: '1'; b: '2'; c: '3' }, 'a' | 'b'> // { c: '3' }
*/
export type Omit<T extends object, K extends keyof T> = Pick<
T,
Exclude<keyof T, K>
>;
Now you are ready to write some tests for the types you have defined. dtslint
uses the
$ExpectType
annotation to state the type of the type expression on the next line.
// $ExpectType Pick<{ a: "1"; b: "2"; c: "3"; }, "c">
type Test_01_Omit = Omit<{ a: "1"; b: "2"; c: "3" }, "a" | "b">;
dtslint
will now evaluate the Test_01_Omit
expression and determine the resultant type
to compare it against the type you’ve specified with $ExpectType
. If you’re anticipating
your type to result in a type error then this can be asserted with $ExpectError
. These
are documented in the README for the
dtslint
project.
Next up we can use some of the assertion types from the conditional-type-checks
package
we installed earlier to run some additional unit style tests of the type.
type Test_02_Omit =
| AssertTrue<IsExact<Test_01_Omit, { c: "3" }>>
| AssertFalse<Has<Test_01_Omit, { a: "1"; b: "2" }>>;
Here the assertions state that the final evaluated expression only has one key c
and does
not have the keys a
or b
. There are more conditional types that you can employ documented
in the project README.
Using both of these projects you can test the more advanced types that your projects employ to ensure their continued success against various TypeScript versions.