17

I found an Equals utils mentioned at: https://github.com/microsoft/TypeScript/issues/27024#issuecomment-421529650

export type Equals<X, Y> =
    (<T>() => T extends X ? 1 : 2) extends
    (<T>() => T extends Y ? 1 : 2) ? true : false;

It could be used to check if two types are equal such as:

type R1 = Equals<{foo:string}, {bar:string}>; // false
type R2 = Equals<number, number>; // true

It is hard for me to understand how this works and what does the T mean in the expression.

Could someone please explain this?

jayatubi
  • 1,972
  • 1
  • 21
  • 51

1 Answers1

26

First let's add some parentheses

export type Equals<X, Y> =
    (<T>() => (T extends /*1st*/ X ? 1 : 2)) extends /*2nd*/
    (<T>() => (T extends /*3rd*/ Y ? 1 : 2))
        ? true 
        : false;

Now when you substitute some types for X and Y what this second extends keyword is doing is basically asking a question: "Is a variable of type <T>() => (T extends X ? 1 : 2) assignable to a variable of type (<T>() => (T extends Y ? 1 : 2))? In other words

declare let x: <T>() => (T extends /*1st*/ X ? 1 : 2) // Substitute an actual type for X
declare let y: <T>() => (T extends /*3rd*/ Y ? 1 : 2) // Substitute an actual type for Y
y = x // Should this be an error or not?

The author of the comment you provided says that

The assignability rule for conditional types <...> requires that the types after extends be "identical" as that is defined by the checker

Here they are talking about the first and the third extends keywords. The checker would only allow that x be assignable to y if the types after them, namely X and Y, are identical. If you substitute number for both

declare let x: <T>() => (T extends number ? 1 : 2)
declare let y: <T>() => (T extends number ? 1 : 2)
y = x // Should this be an error or not?

Of course it shouldn't be an error, because there are 2 variables of the same type. Now if you substitute number for X and string for Y

declare let x: <T>() => (T extends number ? 1 : 2)
declare let y: <T>() => (T extends string ? 1 : 2)
y = x // Should this be an error or not?

Now types after extends are not identical, so there will be an error.


Now let's see why the types after extends must be identical for the variables to be assignable. If they are identical, then everything should be clear, because you just have 2 variables of the same type, they will always be assignable to each other. As for the other case, consider the last situation I described, with Equals<number, string>. Imagine this were not an error

declare let x: <T>() => (T extends number ? 1 : 2)
declare let y: <T>() => (T extends string ? 1 : 2)
y = x // Imagine this is fine

Consider this code snippet:

declare let x: <T>() => (T extends number ? 1 : 2)
declare let y: <T>() => (T extends string ? 1 : 2)

const a = x<string>() // "a" is of type "2" because string doesn't extend number
const b = x<number>() // "b" is of type "1"

const c = y<string>() // "c" is of type "1" because string extends string
const d = y<number>() // "d" is of type "2"

y = x
// According to type declaration of "y" we know, that "e" should be of type "1"
// But we just assigned x to y, and we know that "x" returns "2" in this scenario
// That's not correct
const e = y<string>() 
// Same here, according to "y" type this should be "2", but since "y" is now "x",
// this is actually "1"
const f = y<number>()

It's similar if types are not string and number, which have nothing in common, but something more sophisticated. Let's try {foo: string, bar: number} for X and {foo: string} for Y. Note that here X is assignable to Y

declare let x: <T>() => (T extends {foo: string, bar: number} ? 1 : 2)
declare let y: <T>() => (T extends {foo: string} ? 1 : 2)

// "a" is of type "2" because {foo: string} doesn't extend {foo: string, bar: number}
const a = x<{foo: string}>()

// "b" is of type "1"
const b = y<{foo: string}>()

y = x
// According to type declaration of "y" this should be of type "1", but we just
// assigned x to y, and "x" returns "2" in this scenario
const c = y<{foo: string}>()

If you switch the types and try {foo: string} for X and {foo: string, bar: number} for Y, then there will again be a problem calling y<{foo: string}>(). You can see that there is always something wrong.

To be more precise, if X and Y are not identical, there will always be some type that extends one of them, and doesn't extend the other. And if you try to use this type for T you get non-sense. Actually if you try to assign y = x, the compiler gives you an error like this:

Type '<T>() => T extends number ? 1 : 2' is not assignable to type '<T>() => T extends string ? 1 : 2'.
  Type 'T extends number ? 1 : 2' is not assignable to type 'T extends string ? 1 : 2'.
    Type '1 | 2' is not assignable to type 'T extends string ? 1 : 2'.
      Type '1' is not assignable to type 'T extends string ? 1 : 2'.

Since there is always a type that is assignable to one of X and Y and not the other, it is forced to treat the return type of x as 1 | 2, which is not assignable to T extends ... ? 1 : 2, because T could extend this ... or it could not.

This is basically what this Equals type boils down to, hope it's more or less clear, how it works.


UPD 2: I wanted to add another simple example, where a naïve equality check fails, but Equals doesn't. Let X = {a?: number} and Y = {}, then

type NaiveEquals<X, Y> = 
  X extends Y ? Y extends X ? true : false : false

type A = NaiveEquals<{a?: number}, {}> // true
type B = Equals<{a?: number}, {}> // false

Remark: if you want to be meticulous, in theory A should also be false, because {} extends {a?: number} should be false (not all values of type {} are assignable to a variable of type {a?: number}). But TS is "not 100% sound" as it claims, so in TS this is true.

Here for example type {a: string} is assignable to {}, but not to {a?: number}, so when you use it you get an error:

declare let x: <T>() => (T extends {a?: number} ? 1 : 2)
declare let y: <T>() => (T extends {} ? 1 : 2)

// "a" is of type "2" because {a: string} doesn't extend {a?: number}
const a = x<{a: string}>()

// "b" is of type "1"
const b = y<{a: string}>()

y = x
// According to type declaration of "y" this should be of type "1", but we just
// assigned x to y, and "x" returns "2" in this scenario
const c = y<{a: string}>()

UPD 1:

Speaking about why Equals<{x: 1} & {y: 2}, {x: 1, y: 2}> is false

tl;dr As far as I understand, it is an implementation detail (not sure if I should call it a bug, this may be intentionally wrong behaviour, language limitation, performance trade-off or whatever)

Theoretically, of course, this should be true. As I described above, Equals returns false (in theory) if and only if there exists a type C such that C is assignable to one of X and Y, but not to the other. In this case in the example above if you do x = y and stick it in (x<C>() and y<C>()), you get wrong typings. Here, however, this is not the case, everything that is assignable to {x: 1} & {y: 2} is assignable to {x: 1, y: 2}, so in theory Equals should return true.

In practice, however, it seems like typescript implementation takes a lazier approach when deciding whether types are identical. I should note, that this is a bit of speculation, I never contributed to typescript and don't know its source code, but here is what I found during the last 10 minutes, I could totally miss some details, but the idea should be correct.

The file that does the type checking in ts repository is checker.ts (the link leads to the version of the file between ts 4.4 and 4.5, in future this can change). Here line 19130 seems like the place where the T extends X ? 1 : 2 and T extends Y ? 1 : 2 parts are compared. Here are the relevant parts:

// Line 19130
// Two conditional types 'T1 extends U1 ? X1 : Y1' and 'T2 extends U2 ? X2 : Y2' are related if
// one of T1 and T2 is related to the other, U1 and U2 are identical types, X1 is related to X2,
// and Y1 is related to Y2.
// ...
let sourceExtends = (source as ConditionalType).extendsType;
// ...
// Line 19143
if (isTypeIdenticalTo(sourceExtends, (target as ConditionalType).extendsType) && /* ... */) {
  // ...
}

The comment says that these types are related, if, among other conditions, U1 and U2, in our case X and Y, are identical, this is exactly what we are trying to check. On line 19143 you can see the types after extends are being compared, which leads to isTypeIdenticalTo function, which in turn calls isTypeRelatedTo(source, target, identityRelation):

function isTypeRelatedTo(source: Type, target: Type, relation: /* ... */) {
    // ...
    if (source === target) {
        return true;
    }
    if (relation !== identityRelation) {
        // ...
    }
    else {
        if (source.flags !== target.flags) return false;
        if (source.flags & TypeFlags.Singleton) return true;
    }
    // ...
}

You can see that first it checks if they are exactly the same type (which {x: 1} & {y: 2} and {x: 1, y: 2} are not as far as ts implementation is concerned), then it compares their flags. If you look at the definition of the Type type here you will find that flags is of type TypeFlags which is defined here and would you look at that: intersection has it own flag. So {x: 1} & {y: 2} has a flag Intersection, {x: 1, y: 2} does not, therefore they are not related, therefore Equals returns false although in theory it should not.

Alex Chashin
  • 3,129
  • 1
  • 13
  • 35
  • 1
    So, when we compare two conditional types compiler does deep comparison of types which are placed after `extends` keyword? Btw, very good answer – captain-yossarian from Ukraine Aug 28 '21 at 11:19
  • I can't say for sure, I'm not familiar with typescript implementation, maybe it does, maybe there is something a bit more complicated going on – Alex Chashin Aug 28 '21 at 11:20
  • I am still struggling with `what T refers to`? – Alireza Ahmadi Aug 28 '21 at 11:22
  • It is a generic type argument, it doesn't refer to anything until you actually call a function. Like in the examples when you call `x()` this `T` starts to refer to `string`. When this `T` is used in the `Equals` type, you shouldn't think of what it refers to, you should think if a function of type `() => something` is the same (strictly speaking, not the same, but assignable) as function of type `() => somethingElse` – Alex Chashin Aug 28 '21 at 11:26
  • 1
    It maybe a bit complicated to think about these abstract types, I'm not sure how to explain it better. I think only looking at examples and maybe playing with these things yourself will help – Alex Chashin Aug 28 '21 at 11:26
  • 3
    But `T` has nothing to do with either `X` or `Y` – Alex Chashin Aug 28 '21 at 11:27
  • Yes look at this: `export type Equals =(() => T extends X ? 1 : 2)` and `Equals<{foo:string}, {bar:string}>;` as you see `X` is `{foo:string}` and `Y` is `{bar:string}` but what about `T`. we never pass anything to `T` am I right? – Alireza Ahmadi Aug 28 '21 at 11:30
  • 2
    Yes, and we cannot pass anything to it. It's just an intermediate thing that we use as a tool to achieve the goal of creating the `Equals` type. And the `T` type is never assigned something particular, it's just a **generic** type, it is there to be substituted with something some time later when the function is being called. But since the function is never called, because we only use this function as an intermediate means to compare `X` and `Y`, the `T` type is never assigned any concrete type. Again, it's just an intermediate step that helps compare `X` and `Y` – Alex Chashin Aug 28 '21 at 11:32
  • If I understand correctly to summarize the points are: 1. To put a generic type `after the extends keyword`, which is not typical, we need a placeholder generic pattern to do that. That's why `T` is here. 2. Once a type has been put after extend it `must be identical` according the rules you've explained and that's how the `Equal` actually does the comparison. – jayatubi Aug 28 '21 at 13:48
  • BTW. I think the things after question mark, `1 : 2`, are not important. They could be anything and only need to be same in both `T` expressions. I could even use that as `(() => T extends X ? T : T) extends (() => T extends Y ? T : T)` – jayatubi Aug 28 '21 at 14:14
  • Yes, you are right that `1 : 2` is not important, but I can't say it could be anything. Evidently, it won't work if they are the same like `1 : 1` or `number : number`, and I believe it won't work if one of them is assignable to the other, like `{foo: number} : {foo: number, bar: string}`, although I didn't check and I may be wrong. As for `T : T` it also works, because `T` belonging to `x` function is not assignable to `T` belonging to `y` function, so I believe yes, it should be fine. – Alex Chashin Aug 28 '21 at 17:45
  • How about Equals<{x:1}&{y:2}, {x:1, y:2}>, can explain why it isn't true? – MuYunyun Oct 03 '21 at 16:13
  • 1
    @MuYunyun, in theory it should. In practice, implementation of identity checks in typescript is not perfect, I updated the answer to include a lengthier explanation (as far as I understand at least) – Alex Chashin Oct 03 '21 at 17:42
  • use a wrapper for passing the test `type Compute = { [K in keyof T]: T[K] };` `type test = Equals, { x: 1, y: 2 }> // get true` – Руслан Тютин May 28 '22 at 07:09
  • 1
    Thank you for your detailed answer, it really helps a lot. I have one question, do you know why Equals is false? I think for every T, () => T extends any ? 1 : 2 extends () => T extends unknown ? 1 : 2 is true, but it actually is false. I can't think any counter-example which makes false – ZeroCho Feb 14 '23 at 08:20
  • 1
    @ZeroCho, indeed any value is assignable to a variable of type `any` and `unknown`, so I don't think there is a counterexample, in theory `Equals` should be true. These types don't differ in values they take, they only differ in the way compiler treats them. I think similarly to the `Equals<{x:1}&{y:2}, {x:1,y:2}>` it is one of those "unsound" cases, where TS takes a lazy approach in comparing if `any` and `unknown` are identical and treats them as different, even though it leads to theoretically unjustified behavior. – Alex Chashin Feb 14 '23 at 10:05
  • Lovely answer! You could add another really motivating example with `readonly` - e.g. `Equal<{ readonly a: 'A' }, { a: 'A' }>` – Milos Mar 28 '23 at 20:46