#36630Easy

MyEqual

Rebuild the Equal utility that type-challenges uses to grade every other challenge. The obvious mutual-extends check distributes over unions and returns boolean.

The type that grades every other type challenge is worth building once yourself.

Every Expect<Equal<A, B>> assertion you have ever written in these exercises runs through a single utility type. MyEqual<X, Y> has to answer whether two types are the same type, returning the literal true or the literal false, and it has to be strict about it: property order must not matter, parameter names must not matter, but an optional property and a required one that accepts undefined are different.

type A = MyEqual<{ a: number; b: string }, { b: string; a: number }> // true
type B = MyEqual<'a' | 'b', 'b' | 'a'> // true
type C = MyEqual<[number], number[]> // false

Challenge Instructions: MyEqual

Easy

Type Challenges answer check uses a utility type called Equal. Try implementing this utility type yourself.

Hint: you need to consider Distributive Conditional Types. https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types

View on GitHub: https://tsch.js.org/36630

Change the following code to make the test cases pass (no type check errors).

Loading...

Detailed Explanation

The solution:

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

That is a strange-looking three lines, so let us arrive at it from the obvious attempt.

The obvious attempt returns boolean

Two types are the same if each one is assignable to the other, so the first idea is a mutual extends:

[object Object]

This is correct for plain cases and wrong in a way that is easy to miss:

// NaiveEqual<string, string>       = true
// NaiveEqual<string, number>       = false
// NaiveEqual<'a' | 'b', 'b' | 'a'> = boolean

The last line is the problem. When a naked type parameter is checked with extends, TypeScript distributes the conditional: it splits the union, runs the check once per member, and unions the results. The outer check splits X into 'a' and 'b', and the inner check splits Y into 'b' and 'a'. For X = 'a' the inner check evaluates 'b' extends 'a' (false) and 'a' extends 'a' (true), and unions them into boolean.

So NaiveEqual does not even return a verdict for unions. NaiveEqual<boolean, boolean> is boolean for the same reason, and NaiveEqual<never, never> is never, because distributing over an empty union produces an empty union.

Wrapping in a tuple stops distribution

Distribution only happens for a bare type parameter on the left of extends. Put it inside a tuple and the conditional compares the whole thing at once:

type TupleEqual<X, Y> = [X] extends [Y] ? ([Y] extends [X] ? true : false) : false
 
// TupleEqual<'a' | 'b', 'b' | 'a'> = true
// TupleEqual<[number], number[]>   = false

This is a genuine fix and it is enough for the test cases in this challenge. It is still not Equal, because mutual assignability is not the same relation as identity:

[object Object]

any is assignable to string and string is assignable to any, so a check built on assignability has to say yes. To separate any from string you need to ask the compiler whether the two types are identical, and no extends check phrased in terms of the types themselves can do that.

Asking about identity instead

The trick is to stop comparing X and Y directly and compare two functions that mention them:

type FX = <T>() => T extends X ? 1 : 2
type FY = <T>() => T extends Y ? 1 : 2

Inside a generic signature, T extends X ? 1 : 2 cannot be evaluated: T is unknown until the function is called, so the conditional is deferred and stays in the type as an unresolved node. When TypeScript checks whether FX is assignable to FY, it has to relate those two deferred conditionals, and for deferred conditionals it falls back to checking that they are the same node: same check type, same extends type, same branches. That holds only when X and Y are identical types.

Identity is exactly the relation the challenge wants:

Nothing distributes here, because X and Y never appear as the left side of an evaluated conditional. That also means no special case is needed for unions, boolean or never.

One honest caveat: this leans on how the compiler compares deferred conditional types rather than on a documented language rule. It has been stable for years and the upstream repository ships it as Equal, but it is a trick, not a specified feature.

Edge cases the tests cover

This challenge is originally from here.

Share this challenge

Related Challenges

Learn the Concepts

Become a TypeScript Pro

Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.

Or start solving right away: explore all TypeScript challenges