Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature request]type level equal operator #27024

Closed
4 tasks done
kgtkr opened this issue Sep 11, 2018 · 52 comments
Closed
4 tasks done

[Feature request]type level equal operator #27024

kgtkr opened this issue Sep 11, 2018 · 52 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@kgtkr
Copy link

kgtkr commented Sep 11, 2018

Search Terms

  • Type System
  • Equal

Suggestion

T1 == T2

Use Cases

TypeScript type system is highly functional.
Type level testing is required.
However, we can not easily check type equivalence.
I want a type-level equivalence operator there.

It is difficult for users to implement any when they enter.
I implemented it, but I felt it was difficult to judge the equivalence of types including any.

Examples

type A = number == string;// false
type B = 1 == 1;// true
type C = any == 1;// false
type D = 1 | 2 == 1;// false
type E = Head<[1,2,3]> == 1;// true(see:#24897)
type F = any == never;// false
type G = [any] == [number];// false
type H = {x:1}&{y:2} == {x:1,y:2}// true
function assertType<_T extends true>(){}

assertType<Head<[1,2,3]> == 1>();
assertType<Head<[1,2,3]> == 2>();// Type Error

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript / JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. new expression-level syntax)
@AlCalzone
Copy link
Contributor

Here's a working implementation:

/**
 * Tests if two types are equal
 */
export type Equals<T, S> =
	[T] extends [S] ? (
		[S] extends [T] ? true : false
	) : false
;

The only problem is that any is "equal to" everything, except never.

@kgtkr
Copy link
Author

kgtkr commented Sep 11, 2018

@AlCalzone
I know that.(https://github.com/kgtkr/typepark/blob/master/src/test.ts)
There is a problem of not being able to judge any.

example:

type X=Equals<{x:any},{x:number}>;//true

@DanielRosenwasser
Copy link
Member

any is not assignable to never, so you should be able to determine whether or not either side is exclusively any.

@mattmccutchen
Copy link
Contributor

Here's a solution that makes creative use of the assignability rule for conditional types, which requires that the types after extends be "identical" as that is defined by the checker:

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

This passes all the tests from the initial description that I was able to run except H, which fails because the definition of "identical" doesn't allow an intersection type to be identical to an object type with the same properties. (I wasn't able to run test E because I don't have the definition of Head.)

@RyanCavanaugh RyanCavanaugh added the Question An issue which isn't directly actionable in code label Sep 21, 2018
@kgtkr
Copy link
Author

kgtkr commented Sep 22, 2018

Thank you
There was a way

@aleclarson
Copy link

aleclarson commented Apr 11, 2019

The best solution I have to date: spec.ts

Examples

@jituanlin
Copy link

Here's a solution that makes creative use of the assignability rule for conditional types, which requires that the types after extends be "identical" as that is defined by the checker:

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

This passes all the tests from the initial description that I was able to run except H, which fails because the definition of "identical" doesn't allow an intersection type to be identical to an object type with the same properties. (I wasn't able to run test E because I don't have the definition of Head.)

It work, but how?
Could you provide more explanation?
I try to explain it though by Typescript's bivariant behavior or something else.
But I failed, help, pls.

@fatcerberus
Copy link

@jituanlin AFAIK it relies on conditional types being deferred when T is not known. Assignability of deferred conditional types relies on an internal isTypeIdenticalTo check, which is only true for two conditional types if:

  • Both conditional types have the same constraint
  • The true and false branches of both conditions are the same type

@weakish
Copy link

weakish commented Dec 22, 2019

@AlCalzone It seems that function overloads do not work.

type F = (x: 0, y: null) => void
type G = (x: number, y: string) => void

type EqEq<T, S> = [T] extends [S] ? ([S] extends [T] ? true : false) : false

// Function type intersection is defined as overloads in TypeScript.
type OF1 = EqEq<F & G, G & F> // true
type OF3 = EqEq<{ (x: 0, y: null): void; (x: number, y: null): void }, { (x: number, y: null): void; (x: 0, y: null): void }> // true

@mattmccutchen

Function overloads works:

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

type OF4 = EqEqEq<{ (x: 0, y: null): void; (x: number, y: null): void }, { (x: number, y: null): void; (x: 0, y: null): void }> // false

But function type intersection does not work:

type OF2 = EqEqEq<F & G, G & F> // true

@AnyhowStep
Copy link
Contributor

Can we re-open this or something? Because there isn't a way =(

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Jun 15, 2020

#37314 (comment)

Seems like it's fixed in master, with TS 4.0 as the milestone.

So, this doesn't need to be re-opened, I suppose. I forgot to link to that comment sooner, my bad.

@ldqUndefined
Copy link

@jituanlin AFAIK it relies on conditional types being deferred when T is not known. Assignability of deferred conditional types relies on an internal isTypeIdenticalTo check, which is only true for two conditional types if:

  • Both conditional types have the same constraint
  • The true and false branches of both conditions are the same type

where can I find the infomations about the internal 'isTypeIdenticalTo' check? I can't find anything in the typescript official website....

@weakish
Copy link

weakish commented Sep 9, 2020

@ldqUndefined I do not remember it well, but you may find it in the source code (or not, since TypeScript source code changed a lot).

@gogoyqj
Copy link

gogoyqj commented Feb 13, 2021

Here's a solution that makes creative use of the assignability rule for conditional types, which requires that the types after extends be "identical" as that is defined by the checker:

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

This passes all the tests from the initial description that I was able to run except H, which fails because the definition of "identical" doesn't allow an intersection type to be identical to an object type with the same properties. (I wasn't able to run test E because I don't have the definition of Head.)

not work for:

type ExampleV51 = Equals<1 | number & {}, number>; // supposed to be true, but false got

@wvanvugt-speedline
Copy link

wvanvugt-speedline commented May 21, 2021

I devised a type which seems to work for all scenarios, including intersection types. While it lacks the elegance of the other solutions, it appears to perform better than either based on the tests.

type Equals<A, B> = _HalfEquals<A, B> extends true ? _HalfEquals<B, A> : false;

type _HalfEquals<A, B> = (
    A extends unknown
        ? (
              B extends unknown
                  ? A extends B
                      ? B extends A
                          ? keyof A extends keyof B
                              ? keyof B extends keyof A
                                  ? A extends object
                                      ? _DeepHalfEquals<A, B, keyof A> extends true
                                          ? 1
                                          : never
                                      : 1
                                  : never
                              : never
                          : never
                      : never
                  : unknown
          ) extends never
            ? 0
            : never
        : unknown
) extends never
    ? true
    : false;

type _DeepHalfEquals<A, B extends A, K extends keyof A> = (
    K extends unknown ? (Equals<A[K], B[K]> extends true ? never : 0) : unknown
) extends never
    ? true
    : false;

Here is a TypeScript Playground link demonstrating the functionality on all the test cases. If anyone knows how to optimize this code without sacrificing functionality, I'd love to hear.

@JomoPipi
Copy link

JomoPipi commented May 23, 2021

@wvanvugt-speedline
Nice, but it fails with this:

assertNotType<Equals<[any, number], [number, any]>>();

Here's mine (also fails there):

type And<X,Y> = X extends true ? Y extends true ? true : false : false
type IsAny<T> = 0 extends (1 & T) ? true : false

type _Equals<X,Y> = (X extends Y ? 1 : 2) extends (Y extends X ? 1 : 3) ? true : false
type _EqualTuple<X,Y,Z> = X extends [any] ? Y extends [any] ? Equals<X[number], Y[number]> : false : Z
type Equals<X,Y> = _EqualTuple<X,Y, And<_Equals<IsAny<X>, IsAny<Y>>, _Equals<X,Y>>>

playground

@tianzhich
Copy link

tianzhich commented Jun 3, 2021

@jituanlin AFAIK it relies on conditional types being deferred when T is not known. Assignability of deferred conditional types relies on an internal isTypeIdenticalTo check, which is only true for two conditional types if:

  • Both conditional types have the same constraint
  • The true and false branches of both conditions are the same type

where can I find the infomations about the internal 'isTypeIdenticalTo' check? I can't find anything in the typescript official website....

I found this in /node_modules/typescript/lib/typescript.js, by searching isTypeIdenticalTo. There are also some comments that may help someone here:

// 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.

image

But I'm still not very clear what the related mean here? I can't understand the src code of isRelatedTo.

@tianzhich
Copy link

I write some simple tests, seems A related to B means type A can extend from type B.

type Foo<X> = <T>() => T extends X ? 1 : 2

type Bar<Y> = <T>() => T extends Y ? number : number

type Related = Foo<number> extends Bar<number> ? true : false // true

type UnRelated = Bar<number> extends Foo<number> ? true : false // false

georgefst added a commit to hackworthltd/primer-app that referenced this issue Mar 2, 2023
`Equals` and `assertType` are inspired by, respectively, microsoft/TypeScript#27024 (comment) and microsoft/TypeScript#27024.

We carry on using standard ReactFlow edges and handles, for now. Though we may do something similar there eventually. EDIT: actually, we now define `PrimerEdge` for sort of forwards-compatibility

This also puts us in slightly better shape to ditch ReactFlow for an alternative, should we want to, since we now implement better abstractions in some cases ourselves, using ReactFlow's API less extensively thoughout the codebase
georgefst added a commit to hackworthltd/primer-app that referenced this issue Mar 8, 2023
As described in source comments, this is similar to ReactFlow's `Node`, but more type-safe. We are able to statically assert that the `data` fields of our nodes match what we tell ReactFlow to expect.

`Equals` and `assertType` are inspired by, respectively, microsoft/TypeScript#27024 (comment) and microsoft/TypeScript#27024.

We carry on using standard ReactFlow edges and handles, for now. Though we will likely do something similar there in future, once we require further customization. In fact, we now define a very simple `PrimerEdge` type for a small degree of forwards-compatibility.

This also puts us in slightly better shape to ditch ReactFlow for an alternative, should we wish to, since we now implement better abstractions in some ways ourselves, using ReactFlow's types less extensively.
georgefst added a commit to hackworthltd/primer-app that referenced this issue Mar 13, 2023
As described in source comments, this is similar to ReactFlow's `Node`, but more type-safe. We are able to statically assert that the `data` fields of our nodes match what we tell ReactFlow to expect.

`Equals` and `assertType` are inspired by, respectively, microsoft/TypeScript#27024 (comment) and microsoft/TypeScript#27024.

We carry on using standard ReactFlow edges and handles, for now. Though we will likely do something similar there in future, once we require further customization. In fact, we now define a very simple `PrimerEdge` type for a small degree of forwards-compatibility.

This also puts us in slightly better shape to ditch ReactFlow for an alternative, should we wish to, since we now implement better abstractions in some ways ourselves, using ReactFlow's types less extensively.
FukudaYoshiro added a commit to FukudaYoshiro/superstruct that referenced this issue Mar 23, 2023
`boolean` is effectively the union type `true | false` but the current
`isMatch` implementation will treat overlapping union types as matching.

This patch introduces a more strict type equivalence check based on
microsoft/TypeScript#27024 (comment)
and uses it when checking types that extend `boolean` for equivalence to
`boolean`.

The test added in this patch fails without the corresponding code changes
here.

Fixes #754.
@gfx
Copy link

gfx commented Apr 7, 2023

The "type-level equality operator" should be incorporated as a standard type function in TypeScript, as it constitutes a fundamental aspect of type computations.

Numerous valuable suggestions have been made in the comments, particularly "Matt's Equals<X, Y>," which works effectively in most cases, though it is not flawless. While some alternatives may outperform Matt's implementation, they tend to be overly complex. Therefore, I advocate for integrating this functionality directly into the TypeScript compiler.

@unional
Copy link
Contributor

unional commented Apr 7, 2023

Hi, I mentioned in another thread, but not in this one.
That the type-plus Equal<A,B> works in every single cases that I know of, from my tests and the discussions in these thread.

Feel free to check it out:
https://github.com/unional/type-plus/tree/main/ts/equal

mmkal added a commit to mmkal/expect-type that referenced this issue May 10, 2023
…xclusively (#21)

Fixes #29
Fixes #26
Fixes #5

> Note: I extracted a very small part of this PR to
#20

This is a breaking change as I opted to remove the types that were no
longer needed. They are exported though so it's likely some people
depend on them. I can add these back as desired.

This took a lot of tinkering. This topic and this equality check is
discussed extensively at
microsoft/TypeScript#27024

The main three edge-cases this implementation worked around are:
1. Explicitly handling `any` separately
2. Supporting identity unions
3. Supporting identity intersections

The only remaining known issue with this implementation is:

```ts
  // @ts-expect-error This is the bug.
  expectTypeOf<{foo: number} & {bar: string}>().toEqualTypeOf<{foo: number; bar: string}>()
```

@shicks and I could not find a tweak to the `Equality` check to make
this work.

Instead, I added a workaround in the shape of a new `.simplified`
modifier that works similar to `.not`:

```ts
  // The workaround is the new optional .simplified modifier.
  expectTypeOf<{foo: number} & {bar: string}>().simplified.toEqualTypeOf<{foo: number; bar: string}>()
```

I'm not entirely sure what to do with documenting `.simplified` because
it's something you should never use unless you need it. The simplify
operation tends to lose information about the types being tested (e.g.,
functions become `{}` and classes lose their constructors). I'll
definitely update this PR to reference the `.simplified` modifier but I
wanted to get a review on this approach first. One option would be to
keep around all the `DeepBrand` stuff and to have `.deepBranded` or
something being the modifier instead. That would have the benefit of
preserving all the exported types making this less of a breaking change.

---------

Co-authored-by: Misha Kaletsky <15040698+mmkal@users.noreply.github.com>
Co-authored-by: Misha Kaletsky <mmkal@users.noreply.github.com>
@Mario-Marion
Copy link

I write some simple tests, seems A related to B means type A can extend from type B.

type Foo<X> = <T>() => T extends X ? 1 : 2

type Bar<Y> = <T>() => T extends Y ? number : number

type Related = Foo<number> extends Bar<number> ? true : false // true

type UnRelated = Bar<number> extends Foo<number> ? true : false // false

x1 and x2, y1 and y2 must keep the identical types, otherwise the following problems will occur:

type Foo<X> = <T>() => T extends X ? 1 : 2

type Bar<Y> = <T>() => T extends Y ? number : number

type Related = Foo<string> extends Bar<boolean> ? true : false // true

@unional
Copy link
Contributor

unional commented May 23, 2023

x1 and x2, y1 and y2 must keep the identical types, otherwise the following problems will occur

@Mario-Marion feel free to check out or use the implementation in type-plus. It's working correctly in those cases.
If you want to have a different implementation, that's ok too, look forward to see a different approach and see if there are any cases can be uncoverered.

@craigphicks
Copy link

Equals<string[]&number[], never[]>; // false
Equals<(string[]&number[])[0], never[][0]>; // true

@craigphicks
Copy link

craigphicks commented Dec 12, 2023

@jituanlin AFAIK it relies on conditional types being deferred when T is not known. Assignability of deferred conditional types relies on an internal isTypeIdenticalTo check, which is only true for two conditional types if:

* Both conditional types have the same constraint

* The true and false branches of both conditions are the same type

It seems like saying

sameConstraintIn =  (set of all t s.t. extends X) === (set of all t s.t. extends Y)  
sameConstraintOut =  (set of all t s.t. notExtends X) === (set of all t s.t. notExtends Y)  
is equal = sameConstaintIn && sameConstraintOut

but in set theory that is the same as

X extends Y => for all x in X, x is in Y
Y extends X => for all y in Y, y is in X
isequal = X extends Y && Y extends X

The reason they are different lies in the implementation details only.

I suppose some time saving shortcuts taken with X extends Y && X extends Y are not taken with the more complicated version of equality. But there is no clue in the more complicated form as to what those shortcuts are.

Of course the more complicated version looks as unintelligible as Latin, so Geeks fluent in Latin love it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests