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?
First let's add a couple of 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 actually 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 "1" 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:
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 intended)
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With