DEV Community

Acid Coder
Acid Coder

Posted on • Edited on

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

Top comments (4)

Collapse
 
bwca profile image
Volodymyr Yepishev

Interesting, but how would you embed it into a jest/mocha/whatever the framework test case?

The only way I see is somehow modifying the assert function to return a value which can be then checked in the framework's expect block, but how to do it? Could go with

export const assert = <T extends true | false>(arg : T extends true ? true : false) => arg
Enter fullscreen mode Exit fullscreen mode

and later with

expect(assert<Expect<GetCountOfSubString<'a--a--aa--a', 'a'>, 5>> (true)).toBeTruthy()
Enter fullscreen mode Exit fullscreen mode

in jest or whatever, but then the intellisense of the argument defeats the check purpose, because you can't get an failed test :/

Collapse
 
tylim88 profile image
Acid Coder

interesting, I am not aware of this is possible, do you have link to the source?

Collapse
 
bwca profile image
Volodymyr Yepishev

I am not aware if it is possible too, we could 'force' to pass proper boolean based on what assert generic returns, like this. Though it looks like a dirty hack :)

Thread Thread
 
tylim88 profile image
Acid Coder

if this is possible, I think maybe they use typescript compiler api

I am not familiar with the compiler api, it is quite tedious to be honest and is not properly documented