Warning: This is experimental work. Read the comments for more information on its limitations.
In this blog post, we’ll examine how we can test static types in TypeScript (inferred types, etc.). For example, given the following function:
function createPoint(x: number, y: number) {
return {x, y};
}
We’d like to check in a unit test that TypeScript infers this return type:
{x: number, y: number}
In order to do that, we need a few tools that we are going to look at first.
A conditional type lets you switch between two types, depending on whether a given type fulfills a type assertion:
«Type» extends «TypeAssertion» ? «ThenType» : «ElseType»
In this case, “fulfilling” means: Is Type
a subtype of TypeAssertion
? If you think of types as sets then “subtype of” means “subset of”.
At the moment (due to limitations of TypeScript type inference), conditional types are mainly useful for computing with types. And not, e.g., to type the results of functions.
In the following example, we store the result of computing with types in a type Result
:
type Result = RegExp extends Object ? true : false;
// type Result = true;
Note that true
and false
are types here (so-called literal types).
In the following example, we use the parameterized type YesNo
like a function for types. T
is a type parameter.
type YesNo<T extends boolean> = T extends true ? 'yes' : 'no';
type yes = YesNo<true>;
// type yes = 'yes';
type no = YesNo<false>;
// type no = 'no';
We can use conditional types to test static types. For example, via the following parameterized type:
type AssertIsString<S> = S extends string ? true : never;
This type has a parameter S
whose value is a static type. If that type is a subtype of string
, the result of using AssertIsString
is the type true
. Otherwise, it is the type never
. If a variable has that type then no real value can be assigned to it.
This is an example of using AssertIsString
– we want to check that strValue
does have the statically inferred type string
(or a subtype):
const strValue = 'abc';
const cond1: AssertIsString<typeof strValue> = true;
// No type error: the assertion is true
The next example checks if numberValue
does have the type string
and fails.
const numberValue = 123;
// @ts-ignore: Type 'true' is not assignable to type 'never'.
const cond2: AssertIsString<typeof numberValue> = true;
As an aside, the following simplification of this technique does not work:
type cond3 = AssertIsString<typeof numberValue>;
// type cond3 = never;
The condition of AssertIsString
goes to the else branch, but the resulting type never
is not in conflict with anything and does not produce an error.
TypeScript provides several parameterized utility types. One of them is relevant for us here:
ReturnType<T>
Its result is the return type of a function type. In the next example, we use it to assert that the return type of twice()
is string
:
function twice(x: string) {
return x + x;
}
const cond: AssertIsString<ReturnType<typeof twice>> = true;
The following parameterized type builds on the ideas we have already seen and checks whether a given type T
is equal to an expected type Expected
:
type AssertEqual<T, Expected> =
T extends Expected
? (Expected extends T ? true : never)
: never;
We are checking two conditions:
T
a subtype of Expected
?Expected
a subtype of T
?If both are true then T
is equal to Expected
.
AssertEqual
in action The following example uses AssertEqual
:
function createPoint(x: number, y: number) {
return {x, y};
}
const cond1: AssertEqual<
typeof createPoint,
(x: number, y: number) => {x: number, y: number}
> = true;
We can also just check the inferred return type:
const cond2: AssertEqual<
ReturnType<typeof createPoint>,
{x: number, y: number}
> = true;
$ExpectType
.