Typescript Test Your Generic Type Part 1

tylim88

Acid Coder

Posted on June 28, 2022

Typescript Test Your Generic Type Part 1

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

playground

type test 1

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, and nothing is invalid about it, so typescript does not complain.

so we need 2nd part, the assertion


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

Enter fullscreen mode Exit fullscreen mode

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 matter

type 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
Enter fullscreen mode Exit fullscreen mode

type test 2

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
Enter fullscreen mode Exit fullscreen mode

playground

type test 3

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 matter

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
Enter fullscreen mode Exit fullscreen mode

type test 4

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 matter

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
Enter fullscreen mode Exit fullscreen mode

type testing 6

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

type test 5

there are 2 ways to solve it:

first we can export them
type test 6

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
Enter fullscreen mode Exit fullscreen mode

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

💖 💪 🙅 🚩
tylim88
Acid Coder

Posted on June 28, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related