Search code examples
typescripttypechecking

How to use `IsEqual` to create a utility that asserts the equality of type types


I write in TypeScript and also use the type-fest npm package.

In my code, for documentation purposes, I sometimes like to add assertions that two types are indeed equal or unequal. E.g. like the following:

const b: IsEqual<{a: 1}, {a: 1}> = true;
console.log(b);

If the types passed to IsEqual weren't equal the first line in the above code would fail during type-checking by the TS compiler. The second line is just to get rid of the TS error variable b is defined but not used.

So far so good.

Now I want to create a small utility that would allow me to assert the equality of two types even more succinctly, like so:

function type_fest_is_equal_assertion<T, S>(): void {
      let R: IsEqual<T, S> = true; // * 
      console.log(R);
}


 // with the idea that I can then simply write:
type_fest_is_equal_assertion<{a: number}, {a: number}>()

Unfortunately, the above code fails during compilation at the line marked with the asterisk (*) with:

Type 'boolean' is not assignable to type 'IsEqual<T, S>'.

Playground is here.

Is there a way to accomplish what I am seeking?

Tangentially, I am also seeking clarification for the above error message in this related question.


Solution

  • The only way this could work is if your generic function constrains one of its type parameters so that IsEqual<T, S> is known to be true. This runs the risk of making your constraints illegally circular, so we have to be careful. One approach looks like

    function typeFestIsEqualAssertion<
      T extends IsEqual<T, S> extends true ? unknown : never,
      S extends IsEqual<T, S> extends true ? unknown : never
    >(): void {
      console.log(true);
    }
    

    which luckily compiles without error.

    Essentially, both T and S are constrained to IsEqual<T, S> extends true ? unknown : never. When you call the function and specify T and S, if IsEqual<T, S> is true, then this constraint looks like T extends unknown, S extends unknown, which will always work because unknown is a universal supertype in TypeScript. On the other hand, if IsEqual<T, S> is not true, then this constraint looks like T extends never, S extends never, which will almost always fail because never is the universal subtype in TypeScript. The only type assignable to never is never itself. This means you'll get your expected behavior as long as at least one of S and T is not specified as never. And if both are never then presumably IsEqual<never, never> would be true and you'd be happy that it's accepted:

    typeFestIsEqualAssertion<{ a: 1 }, { a: 1, b: 2 }>(); // error
    typeFestIsEqualAssertion<{ a: 1 }, { a: 1 }>(); // okay
    typeFestIsEqualAssertion<{ a: 1 }, { b: 2 }>(); // error
    
    typeFestIsEqualAssertion<{ a: 1 }, never>(); // error
    typeFestIsEqualAssertion<never, { a: 1 }>(); // error
    typeFestIsEqualAssertion<never, never>(); // okay
    

    Note that the error messages will all say that the specified type doesn't satisfy the constraint never, which might or might not be acceptable for your use case. If you need something more descriptive you'll need to work around the lack of "invalid types" or "throw types" in TypeScript. Look at microsoft/TypeScript#23689 for a discussion of that.

    Playground link to code