Search code examples
typescripttypechecking

what is the type of IsEqual in `type-fest`?


I am writing in TypeScript and use type-fest for some additional type checking.

The following code:

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

… fails at the line commented with the asterisk (*) with:

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

The error message makes no sense to me. I though that IsEqual is a type alias that evaluates to a boolean type: true or false. How should I read this error message to make it make sense?

Incidentally, motivation for writing the above function is given here.


Solution

  • IsEqual<T, S> is implemented via conditional type, so inside the body of your generic function, it is a generic conditional type. Rather than rely on a third-party library I'm going to just make my own version for demonstration purposes:

    type Eq<T, S> =
      [T] extends [S] ? [S] extends [T] ? true : false : false
    
    function f<T, S>(): void {
      let R: Eq<T, S> = true; // error
      console.log(R);
    }
    

    (No, it's not exactly the same, but it demonstrates the same issue. Feel free to replace.) So Eq<T, S> will either be true or false, depending on T and S. Inside the body of f(), the compiler does not know much else. T and S are not known, and so Eq<T, S> represents some unknown subtype of boolean. It is not the same as boolean. If you want a value of type boolean I can safely give you a value of type Eq<T, S>, but the reverse is decidedly unsafe. We don't know if Eq<T, S> will be true or false, so we cannot assign true to it.

    For example, what if someone makes the following call?

    f<string, number>(); // no error
    

    Now we can see that Eq<T, S> will be Eq<string, number> which is false. So R would be of type false and it would be a big mistake to allow assigning true to it. Of course it is possible to call f<string, string>() and then R would be true, but that's not guaranteed inside the body of f().


    Even if you constrain the S and T generics in the call signature of f() so that you are sure Eq<T, S> will be true and cannot be false, the compiler is unlikely to be able to perform the same reasoning. Generic conditional types are quite often completely opaque to the compiler, so you'll still get such errors:

    function f<
      T extends (Eq<T, S> extends true ? unknown : never),
      S extends (Eq<T, S> extends true ? unknown : never)
    >(): void {
      let R: Eq<T, S> = true; // still error
      console.log(R);
    }
    
    f<string, number>(); // error now
    

    So that's what's going on. An even simpler demonstration is:

    function f<R extends boolean>() {
      let R: R = true; // error
    }
    

    The compiler doesn't know that true is assignable to R. All it knows is that R is assignable to boolean, which is the opposite direction from what you need. It's almost impossible to safely assign a specific type to a generic one.

    Playground link to code