John Reilly
John Reilly

Reputation: 6269

TypeScript: testing conditional type equality - understanding the syntax

I've been experimenting with comparing the equality of types within TypeScript. I've happened upon this approach:

type SomeType = {
  property: string;
}

type OtherType = {
  property: string;
}

/**
 * The two types passed are evaluated for equivalence; returning true if they
 * are equivalent types and false if not
 * 
 * Based upon Matt McCutchen's comment:
 * https://github.com/microsoft/TypeScript/issues/27024#issuecomment-421529650
 */
type Equals<Type1, Type2> =
    (<Type1Match>() => Type1Match extends Type1 ? "match!" : "not a match") extends
    <Type2Match>() => Type2Match extends Type2 ? "match!" : "not a match"
    ? true
    : false;

type IsTrue<T extends true> = T

/**
 * This expression will evaluate that our types match; note
 * the underscore at the start which indicates this is expected
 * to be an unused type 
 */
type test = IsTrue<Equals<SomeType, OtherType>

I'm reading and re-reading Equals above and I'm slightly puzzled by the <Type1Match>() => at the start of (<Type1Match>() => Type1Match extends Type1 ? "match!" : "not a match").

There doesn't appear to be a function in the mix here; or is there and am I missing something? Is this just a backdoor way to introduce a new generic?

Update following Titians's excellent answer

The unfortunate thing about Titian's updated (and clearer) approach is that it doesn't handle all type equivalence checks well. Consider any comparisons where Titian's Equals2 fails to detect differences. The same is true for another simplified Equals3 that I put together.

But Equals does succeed in detecting these issues. The question in my mind is "why does this work?" and also "can I rely on this moving forwards?". So, if I've got code that depends upon being able to perform this equality check, am I building upon shifting sand here? Does this approach rely upon a compiler implementation detail that might change in future?

See in playground

type SomeType = {
  property: any;
}

type OtherType = {
  property: string;
}

/**
 * The two types passed are evaluated for equivalence; returning true if they
 * are equivalent types and false if not
 * 
 * Based upon Matt McCutchen's comment:
 * https://github.com/microsoft/TypeScript/issues/27024#issuecomment-421529650
 */
type Equals<Type1, Type2> =
    (<Type1Match>() => Type1Match extends Type1 ? "match!" : "not a match") extends
    <Type2Match>() => Type2Match extends Type2 ? "match!" : "not a match"
    ? true
    : false;


type Equals2<Type1, Type2> =
    (<Type1Match extends Type1>() => Type1Match) extends (<Type2Match extends Type2>() => Type2Match)
    ? true
    : false;

type Equals3<Type1, Type2> =
    [Type1] extends [Type2] ? (
        [Type2] extends [Type1] ? true : false
    ) : false;

type IsTrue<T extends true> = T

/**
 * This expression will evaluate that our types match; note
 * the underscore at the start which indicates this is expected
 * to be an unused type 
 */
type test = IsTrue<Equals<SomeType, OtherType>> // errors as the types are not equivalent
type test2 = IsTrue<Equals2<SomeType, OtherType>> // unfortunately does not error
type test3 = IsTrue<Equals3<SomeType, OtherType>> // unfortunately does not error

Upvotes: 2

Views: 610

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249556

<Type1Match>() => Type1Match extends Type1 ? "match!" : "not a match" is actually a function signature and Type1Match is a type parameter to the function signature. Removing the conditional type from the return type makes it a bit more clear <Type1Match>() => SomeReturnType. The return type is a conditional type Type1Match extends Type1 ? "match!" : "not a match" (which does make it indeed very difficult to read).

The reason for all of this is that TS uses the equivalence comparison in a very limited number of places. This is one of them, in conditional types that involve type parameters.

Upvotes: 2

Related Questions