type-testing
v0.2.0
Published
🌱 A micro library for testing your TypeScript types
Downloads
5,210
Maintainers
Readme
type-testing
🌱 A micro library for testing your TypeScript types.
Sometimes your project's TypeScript types really matter. More and more often, the TypeScript types themselves are a core part of the product. But there hasn't been a good way to make sure the types don't subtly break from day to day. This is the problem type-testing
solves.
A lot of popular projects with incredible type inferencing already use the code in type-testing
. For example Zod, TanStack Query, zustand, tRPC, MUI, type-fest, ts-reset, and the TypeScript Challenges all have variants of the same code. We collected that code here and wrote tests for them (yes: test inception).
Goals
- Bring this commonly copy-pasta'd code into one place where the utilities are tested and correct.
- Demonstrate how to test types in TypeScript project.
| Before testing your types | After using type-testing
|
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|
| :dizzy_face: Your inferred types work great on Monday, but by Friday you realize they don't infer correctly anymore. | :green_salad: At 11:53am on Wednesday, a type test fails and you realize you should probably stop to eat some lunch before you break anything else. |
| :see_no_evil: You want to make awesome TypeScript types.. but every time you try to add or fix something, you are left wondering if you broke something else. | :mechanical_arm: You can refactor with confidence. |
| :crossed_fingers: Your open source project can be tough to keep up with. You want to accept community PRs but you're not sure if they possibly break something. | :partying_face: A community PR won't break anything, and you can request new tests that cover any areas of concern. |
| :clown_face: You wonder about what will happen if there's some weird TypeScript edge-case where never
or unknown
or any
is passed to your type by an end-user. | :mage: You don't have to be a TypeScript wizard to write great TypeScript types. You can write a test and be sure things will work as expected! |
Quickstart
What if you have a fancy TypeScript function with an inferred type in your library:
const shoutItOutLoud = <T extends string>(str: T) => (
`${str.toUpperCase() as Uppercase<T>}!!!` as const
);
The type of this function isn't just string
, it's the actual result as a TypeScript string literal. But how do you write a test for that behavior of your library?
Now you can write a test for the type for your function!
import { Expect, Equal } from "type-testing";
const hello = shoutItOutLoud('hello');
type test_hello = Expect<Equal<typeof hello, 'HELLO!!!'>>;
You can do this right alongside your regular tests!
import { Expect, Equal } from 'type-testing';
import { shoutItOutLoud } from './shout';
describe('shoutItOudLoud', () => {
it('has the intended API surface', () => {
const hello = shoutItOutLoud('hello');
// this tests the type
type test_hello = Expect<Equal<typeof hello, 'HELLO!!!'>>;
// this tests the runtime behavior
expect(hello).toEqual('HELLO!!!');
});
});
[!IMPORTANT] Now, if your fancy type isn't exactly correct, your test suite will fail!
just like you'd want it to
Example: type-only libraries
Consider, though, that many libraries are shipping TypeScript types as part of the product. In this case, they have very little way to be sure that the types they're shipping to their users are working as intended. Consider the above but written in types:
type ShoutItOutLoud<T extends string> = `${Uppercase<T>}!!!`;
type Hello = 'hello';
// Compiler error! this should be `HELLO!!!` (all caps)
type test_Hello = Expect<Equal<ShoutItOutLoud<Hello>, 'HeLLO!!!'>>
protip: you can see some great examples of what these kinds of test looks like in this very repo itself.
Example: testing functions
Now, you can write tests that will lock-in the intended behavior of your types.
If someone changes a type parameter or return type accidentally, now you'll know about it right away!
type test_params = Expect<Equal<
Parameters<typeof shoutItOutLoud>,
[string]
>>;
type test_return = Expect<Equal<
ReturnType<typeof shoutItOutLoud>,
`${Uppercase<string>}!!!`
>>;
:family_man_woman_girl_boy: Your New Type Testing Family
Also, if you just install and start using the library you'll discover that every type has lots of JSDoc description to help you along the way!
FAQ
Why not just rely on explicit return types?
Lots of reasons.
- Because a lot of times you can't. What you ship is often an abstraction (i.e. generic), so you really need to write a specific unit test to make sure that your end users of your library have the right type experience (whether those users are external to your team or not).
- Because being able to write these tests while you're working enables you to do TDD with the types themselves. Even if you don't do full-blown 100% TDD, it's pretty useful to be able to be sure that you've got your core use-cases covered. Then, you can refactor and improve your code with a lot more confidence.
- Because return types can lie.
Is this a new idea?
Nope! It's been knocking around in the TypeScript community for a while, but there has yet to be someone to write tests for these types (ironically) and package them into a micro library.
Can Expect
and Equal
be combined to a type ExpectEqual
?
Unfortunately, no. The power of this approach taken in this library is that it will error at build time while being type checked, and currently there's no way to combine the two utilities.
If you do happen to find a way... we're all waiting to hear about it! File an issue!!
Where are all the aliases?
"no API is the best API".
You may have seen a version of this code copied around that contained aliases for Expect
like IsTrue
and ExpectTrue
, as well as aliases for ExpectFalse
like IsFalse
. The hope is to avoid people having to learn or memorize the quirks of different assertion APIs. This is still a pretty cutting-edge part of the TypeScript world, but if you look around, you're going to find that 98% of the time (or more!) you just need Expect
and Equal
.
What about _____ other way of testing types?
You might be familiar with other projects that attempt to do something similar. Here's a quick overview:
eslint-plugin-expect-type
is powerful, but relies on comments and requires ESLint. This means that refactoring and renaming will get out of sync because the tests themselves aren't actually code. On top of that, there's a problem with the order of unions in TypeScript not being stable from release-to-release, which causes very annoying false positive test failures that you have to manually fix.tsd
is nice, but it's not for the type layer itself. For that matter, there are a few nice alternatives if you can integrate this into your test runner, e.g. Jest's Expect type is built into it's assertion functions. The same goes forexpect-type
.ts-expect
is probably the most similar currently existing thing but it is built more for things that have runtime manifestations (i.e. things that JavaScript, unlike TypeScript types). The code in this library is already battle tested and used by lots of production projects.
What about ESLint/TypeScript complaining of unused variables?
If you have noUnusedLocals enabled, you can safely disable it just for your tsconfig.eslint.json
. It can still be left on for building for production.
For ESLint, there's a more powerful way to do it which is just to turn off this specific pattern for test files:
/** @type { import('@typescript-eslint/utils').TSESLint.Linter.Config } */
const config = {
overrides: [
{
files: ['**/*.test.ts', '**/*.test.tsx'],
rules: {
'@typescript-eslint/no-unused-vars': [
'error',
{
vars: 'all',
varsIgnorePattern: 'test_.*', // TypeScript type tests
argsIgnorePattern: '_',
},
],
},
},
],
};
So, it looks like TypeScript has jumped the shark, eh? 🦈🌊🦈
Well. This level of type testing isn't "for everyone". It's largely for library authors or projects that have very powerful inferred types.
If:
- you don't use a lot of generics
- you aren't using TypeScript
strict
mode - you use
any
a lot - your codebase has hundreds of
// @ts-ignore
lines - your library or project doesn't mind breaking changes at the type layer
...then this library might not be something you'd benefit from introducing.
But as we start seeing projects like Zod, where the types "working" is fully half of the entire project (or type-fest where it's the whole project).. it starts to feel a little lopsided to be able to write tests and assertions about the JavaScript part, but not as much about the TypeScript part.
Why not just pass the type to the generic of expect
?
Depending on your testing library, you may be able to do something like this:
expect<'HELLO!!!'>(hello).toEqual('HELLO!!!');
This approach can work, but there are lots of libraries that operate exclusively at the type level. They could never use such an approach because they don't have any runtime code with which to test.
You also put yourself at the mercy of the error message generated by the expect types. With type-testing, it's quite clear that the types are wrong, and therefore much more clear what you need to fix.