Typescript has some nice utility types to help you improve code typing.
As a reminder, utility types, are like functions but for types. What this means is that utility types take types
as parameters and return types
as output.
For example the utility named Parameters<T>
is defined like this :
type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
It takes any function type as a parameter and returns a Tuple
of all the function parameters.
Sometimes you need to create your own custom utility types.
And if usually you add some unit tests for your functions, you might also want to do the same for your utility type.
But how to test them ? Does this mean that tests are run at compile time and fail to report errors at runtime ? can you do these tests with your favorite test Runner ?
Utility to compare two types
First of all, if you want to test your utility types, at the minimum we will need to compare the output of your utility type to the expected one.
First try
One could try to do this :
type Equals<T,U> = T extends U ? U extends T ? true : false : false;
This will work until you realise that this returns true
:
type Test = Equals< 1, 1 | 2 >;
This is due to how typescript conditional types work. It's doing distributive conditional typing.
Since we want to do strict type equality, we want to disable distributive behaviour.
Disabling distributive conditional typing
One way of doing it is to hide the type you want to test inside. For example, like this :
type Equals<T,U> = [T] extends [U] ? [U] extends [T]? true: false : false;
Now :
type Test = Equals< 1, 1 | 2 >;
returns false
.
But we are not there yet. Indeed, because of any, this will return true :
type Test = Equals< any, 1 | 2 >;
Solution
type Equals<T, U> =
(<V>() => V extends T ? 1 : 2) extends
(<V>() => V extends U ? 1 : 2) ? true : false;
This solution relies on internal understanding of Typescript isTypeIdenticalTo
. For a more detailed explanation, you can check this issue
How to use it to run your utility type tests
When writing tests, you need to have something to help you diagnose the issue, a readable message.
So you can write the previous test like this :
type Assert<T, U> =
(<V>() => V extends T ? 1 : 2) extends
(<V>() => V extends U ? 1 : 2) ? true :
{ error: "Types are not equal"; type1: T; type2: U };
Now when Assert
checks that two types are not equal, you'll get a nice message.
And you can use it like this :
const test1: Assert<Parameters<(a: number)=>void>,[number]> = true; // pass
const test2: Assert<Parameters<(a: number)=>void>,[string]> = true; // fail
Conclusion
By using this Assert
utility type (you can even use it to test itself) you'll be able to add unit tests for your own utility types.
This will show you a nice message as to why it fails and where. It will allow you to monitor typescript regressions in the type system for your utility types.
if you want to see it in practice, you can check some tests here for zodios library.
If this article was helpfull to you, don't forget to give it a thumbsup.
Top comments (0)