Testing static types in TypeScript

[2019-07-11] dev, typescript
(Ad, please don’t block)

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.

Resources you may find useful while reading this blog post  

Conditional types  

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.

A simple example  

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).

Parameterized types as functions for 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';

Asserting types via conditional types  

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.

Extracting return types  

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;

Generic type tests  

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:

  • Is T a subtype of Expected?
  • Is 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;

Further resources