There are a lot of use-cases where we want to check for equality of two types in typescript. A straightforward way to do this is to use the extends
keyword. like
Here, we check if T
extends U
and U
extends T
. If both are true, then the types are equal. Straightforward right? Not so fast!
This will work for most of the cases, but there are some edge cases where this will not work. For example, if we have a type like
In this case, AreEqual<A, B>
will return true
which is not what we want. We want it to return false
because the types are not equal. This returned true because
B
extendsA
is true, becauseB
has all the properties ofA
A
extendsB
is true, because A is a subset ofB
The extends keyword in TypeScript is used to check if one type is assignable to another. However, it doesn’t provide a direct way to compare two types for equality. This can lead to unexpected results when comparing complex types with different structures.
So, how do we fix this? We can use the keyof
keyword to check if the keys of both types are the same. Like
Also, we do not want to check only for the object (Record) types, we also want to check for the primitive types. For strings, numbers, booleans, etc. A more practical implementation would be
This implementation works for all the cases. It checks if T
extends U
and U
extends T
and also checks if the keys of both types are the same. But how does it work? and why do we introduce two anonymous generic functions? 🤔
Let's break it down.
This approach works because function types are compared structurally in TypeScript. By setting up two generic functions, one that tests G extends T and another that tests G extends U, we can leverage the fact that these two functions will only be the same if T and U are the exact same type.
<G>() => G extends T ? 1 : 2
: This function type will return 1 if G can extend T, and 2 otherwise. For this comparison to work as expected, we need T and U to be exactly the same types, so that the two functions are identical.(<G>() => G extends T ? 1 : 2) extends (<G>() => G extends U ? 1 : 2)
: Here, we use an extends clause to check if the function for T is assignable to the function for U. If they’re the same type, the left side will extend the right side, resulting in true
We need two functions to compare T and U. If we only had one function, we would be comparing G extends T and G extends U, which would always return true because G can extend both T and U. By using two functions, we can ensure that the functions are only the same if T and U are the same.
This implementation of AreEqual
is generally robust, but there are a few edge cases in TypeScript type compatibility it may struggle with. For example:
- When types have different structures but behave similarly in certain conditions (like
{ a: string } and { a: string, b?: number }
). - When comparing complex types that contain conditionals, mapped types, or distributive properties.
In these cases, you may need to adjust the implementation to handle the specific requirements of your types.