Typescript Test Your Generic Type Part 1

Acid Coder
5 min readJun 28, 2022

--

when trying to create tools with typescript, especially aiming for type safety and flexibility, it is common that we end up with many generic types,

some of it even end up with a big chunk of type manipulation logic.

So how can we be confident that our types are working, how can we test our types?

Turn out it is simple and also hard, but we will focus on the simple part first.

let’s take string literal type substring counting as our test subject

type GetCountOfSubString<
String_ extends string,
SubString extends string,
Count extends unknown[] = []
> = String_ extends `${string}${SubString}${infer Tail}`
? GetCountOfSubString<Tail, SubString, [1, ...Count]>
: Count['length']
type NumberOfA = GetCountOfSubString<"a--a--aa--a","a"> // 5

We want to make sure GetCountOfSubString<"a--a--aa--a","a"> always result in 5

basically both should extend each other

next, we create the checker, the checker consist of 2 parts

first is Expect, we want to check whether both types extends each other

type Expect<T, U>= T extends U ? U extends T ? true : false : false type r1 = Expect<GetCountOfSubString<"a--a--aa--a","a">,5> // true, success check
type r2 = Expect<GetCountOfSubString<"a--a--aa--a","a">,1> // false, fail check

playground

so far so good, you get the result you want, the type is true if the result is correct and false if the result is incorrect

but something is missing, when you run type-check with tsc, nothing happens, this is because it simply returns the type as true and false, which is a legit type, so typescript sees nothing wrong in it.

so we need 2nd part, the assertion

type Assert<T extends true> = T // be anything after '=', doesn't matter

applying them

type Expect<T, U>= T extends U ? U extends T ? true : false : false type Assert<T extends true> = T // be anything after '=', doesn't mattertype r1 = Assert<Expect<GetCountOfSubString<"a--a--aa--a","a">,5>> // true, pass test
type r2 = Assert<Expect<GetCountOfSubString<"a--a--aa--a","a">,1>> // false, fail test

now we see that the fail test failed, and when we run tsc, we can see the error in the console.

but wait something is still not right, what is it?

well, a fail test should fail, that is expected, and should not trigger an error

so are we back to square one?

no, we are closer, here is how we solve it, by using the @ts-expect-error comment

type r1 = Assert<Expect<GetCountOfSubString<"a--a--aa--a","a">,5>> // true, pass test
// @ts-expect-error
type r2 = Assert<Expect<GetCountOfSubString<"a--a--aa--a","a">,1>> // false, fail test

playground

there, no more type-checking error

@ts-expect-error only suppress the error if the line has an error, else if you use it on a perfectly ok line, TS will give us an error instead, and this is the behaviour that we want

so let's see if there is a bug in GetCountOfSubString, will this works as expected?

let’s try to fail our pass test:

type GetCountOfSubString<
String_ extends string,
SubString extends string,
Count extends unknown[] = []
> = "BUG!!"
type Expect<T, U>= T extends U ? U extends T ? true : false : false type Assert<T extends true> = T // be anything after '=', doesn't mattertype r1 = Assert<Expect<GetCountOfSubString<"a--a--aa--a","a">,5>> // true, pass test
// @ts-expect-error
type r2 = Assert<Expect<GetCountOfSubString<"a--a--aa--a","a">,1>> // false, fail test

playground

let’s try to fail our fail test:

type GetCountOfSubString<
String extends string,
SubString extends string,
Count extends unknown[] = []
> = 1
type Expect<T, U>= T extends U ? U extends T ? true : false : false type Assert<T extends true> = T // be anything after '=', doesn't mattertype r1 = Assert<Expect<GetCountOfSubString<"a--a--aa--a","a">,5>> // true, pass test// @ts-expect-error
type r2 = Assert<Expect<GetCountOfSubString<"a--a--aa--a","a">,1>> // false, fail test

playground

yup, it works!

but we are not done yet, if you are using linter like eslint, it will complains the type is declared but never used

there are 2 ways to solve it:

first we can export them

or we turn Assert into a function instead

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const assert = <T extends true>() => {
//
}
assert<Expect<GetCountOfSubString<'a--a--aa--a', 'a'>, 5>>() // true, pass test
// @ts-expect-error
assert<Expect<GetCountOfSubString<'a--a--aa--a', 'a'>, 1>>() // false, fail test

playground

the second method is recommended, it is shorter because it doesn’t require us to create a new type for every assertion

that is it for part 1, in part 2 we will take care of some edge cases, which is the hard part

--

--

Acid Coder

Typescript Zombie. Youtube Pikachu On Acid. (Unrelated to programming but by watching it you become a good developer overnight)