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[]> // falseType 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).
The solution:
type MyEqual<X, Y> =
(<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2
? true
: falseThat is a strange-looking three lines, so let us arrive at it from the obvious attempt.
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'> = booleanThe 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.
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[]> = falseThis 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.
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 : 2Inside 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:
{ a: number; b: string } and { b: string; a: number } are the same type, property order is not part of a type.(x: number) => string and (y: number) => string are the same type, parameter names are documentation.any and string are not identical, so the check is false.{ a?: number } and { a: number | undefined } are not identical either, which is why Equal is the assertion helper these exercises use.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.
MyEqual<'a' | 'b', 'b' | 'a'> and MyEqual<number | string, string | number> are true: union members have no order, and the signature trick never splits them.MyEqual<{ a: number; b: string }, { b: string; a: number }> is true, while MyEqual<{ a: number }, { a: number; b: string }> is false: an extra property makes a different type even though the assignability goes one way.MyEqual<[number], number[]> is false: a one-element tuple and an array of the same element type are assignable in one direction only, and they are certainly not identical.MyEqual<(x: number) => string, (y: number) => string> is true but MyEqual<(x: number) => string, (x: string) => string> is false: names are ignored, parameter types are not.MyEqual<true, false> is false, and the boolean literal cases stay literal because nothing widens true to boolean along the way.This challenge is originally from here.
Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges