Skip to content

[Feature request]type level equal operator #48100

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

Open
DetachHead opened this issue Mar 3, 2022 · 21 comments
Open

[Feature request]type level equal operator #48100

DetachHead opened this issue Mar 3, 2022 · 21 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@DetachHead
Copy link
Contributor

DetachHead commented Mar 3, 2022

original issue: #27024

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)

workarounds

to summarize the discussion in #27024, the accepted solution was:

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

however there are many edge cases where it fails:

there were some other workarounds posted that attempted to address these problems, but they also had cases where they didn't work properly.

what "equal" means

i think it's important for it to treat structurally equal types as equal. for example { a: string, b: number; } should be considered equal to { a: string } & { b: number; } as they behave exactly the same

@RyanCavanaugh
Copy link
Member

What I see in that thread is that people have differing definitions of "equal" that are possibly in conflict, e.g. #27024 (comment). If you want to reopen this please fill out something more substantial in the template that goes into the shortcomings, current trade-offs, and desired semantics.

@DetachHead
Copy link
Contributor Author

DetachHead commented Mar 4, 2022

@RyanCavanaugh i don't think there was any disagreement on the definition of "equal" but rather people finding edge cases that the workarounds didn't account for. as far as i can tell everyone in that discussion seems to agree on what types should be considered equal.

regardless, i've updated the issue with more information. can it be re-opened (or can the original issue be re-opened)?

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Mar 4, 2022

{ p: string | number; } should be considered equal to { p: string } | { p: number; }

👀

These types are very much different; T extends { p: string } ? true : false is true | false for one and false for the other

@RyanCavanaugh RyanCavanaugh reopened this Mar 4, 2022
@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Mar 4, 2022
@DetachHead
Copy link
Contributor Author

oops, updated with a better example

@HeavenSky
Copy link

// for example, below also is true
type M = Equal<{ x: 1 } & { y: 2 }, { x: 3, y: 2 }>

@HeavenSky
Copy link

// for example, below also is true,  incorrect
type M = Equal<{ x: 1 } & { y: 2 }, { x: 3, y: 2 }>

So, this solution is not quite right.

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

@ecyrbe
Copy link

ecyrbe commented Jul 23, 2022

// for example, below also is true,  incorrect
type M = Equal<{ x: 1 } & { y: 2 }, { x: 3, y: 2 }>

So, this solution is not quite right.

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

if you want to handle this case. you can use this :

type Simplify<T> = T extends unknown ? { [K in keyof T]: T[K] } : T;

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

type M = Equal<{ x: 3 } & { y: 2 }, { x: 3, y: 2 }>

Playground Link

@DetachHead
Copy link
Contributor Author

unfortunately that approach still isn't foolproof. it doesn't recursively fix the types:

type M = Equal<{a: { x: 3 } & { y: 2 }}, {a: { x: 3, y: 2 }}> //false

@ecyrbe
Copy link

ecyrbe commented Jul 24, 2022

If you want , you can... Just do a recursive simplify

type Simplify<T> =  T extends Record<any,unknown> ?
    { [K in keyof T]: Simplify<T[K]> } : T;

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

type M = Equal<{a: { x: 3 } & { y: 2 }}, {a:{ x: 3, y: 2 }}>;

for a complete handling of all simplify use cases, check here this playground Link

@DetachHead
Copy link
Contributor Author

i jumped down that rabbit hole as well, but i kept finding cases where my solution didn't work. same story with yours, however admittedly it's much cleaner than what i came up with.

// should be false:
const false1: Equal<(() => 2), ((value: number) => 1) & (() => 2)> = false // error
const false2: Equal<[any, number], [number, any]> = false // error
const false3: Equal<<T>(value: T) => T, (value: unknown) => unknown> = false // error

// should be true:
const true1: Equal<(() => 2) & ((value: number) => 1), ((value: number) => 1) & (() => 2)> = true // error
const true2: Equal<number & {}, number> = true // error

we could probably keep adding complexity to the Simplify type until these tests pass too (except maybe the generic one, i have no clue how to fix that), but i think it's an uphill battle which is why i think this needs to be supported in the language

@ecyrbe
Copy link

ecyrbe commented Jul 24, 2022

I understand your point of view. I agree that the false2 should be true. so i corrected it.
I also fixed the function use case with FnA | FnB not equal to FnB.
You can check it Here
But i did it only for sports. In pratice i would not use this monster.
I think doing those in pratice, could really have a bad impact on typescript performance and could maybe be also really complex to implement for typescript team for a really small audience, and that maybe not worth it ?

@geoffreytools
Copy link

As much as I would like to have a reliable equality check, I have to agree with Ryan about people disagreeing on what should be equal.

For instance, I don't think intersection of functions should be equal, because this operation is not commutative:

type A = ((a: number) => string) & ((a: 1|2|3) => 'Maria')
type B = ((a: 1|2|3) => 'Maria') & ((a: number) => string)

declare const fooA:A;
declare const fooB:B;

const barA = fooA(1) // string
const barB = fooB(1) // "Maria"

If you want an equal operator in order to catch bugs in your types, you will probably want Equal<A, B> to be false. If you want it for some other reason, maybe your definition of "equality" in that specific context will be different.

Concerning performance, if this kind of check only appears in a test suite, of course you want the said test suite to run fast, but it is not as critical as making the type checking of your production code fast, so I would not mind a slow workaround if it were reliable.

I have learned to use conditional types in times when using an intersection would have made logical sense, because intersections have lots of quirks. What would "equality" mean in an unsound type system?

@DetachHead
Copy link
Contributor Author

interesting, i wasn't aware that it resolves to the first matching overload instead of the best one.

in that case, i agree that those two types should not be considered equal, according to my definition of "equal" in the OP that the types must "behave exactly the same"

@andrewbaxter
Copy link

andrewbaxter commented Sep 1, 2022

This issue was really hard to follow, since half the discussion is about what type equality means and half is about whether it's possible to work around this gap using existing features. I think the original request for shortcomings, tradeoffs, and semantics is the current blocker.

Doesn't Typescript already internally have a concept of type equality? I think the following definition is simple and would be consistent with other language features: two types A and B are equal if

declare let a: A;
declare let b: B;
a = b;
b = a;

doesn't produce any type checking errors.

The Typescript type checker has no issue with some of these supposedly failing cases: C, D, G. I have no thoughts on whether this is right or wrong, but it seems reasonable that if these are expected to fail then this is a Typescript bug.

@RyanCavanaugh 's test above

type J={ p: string | number; };
type J2= { p: string } | { p: number; };

fails as expected.

type K = ((a: number) => string) & ((a: 1|2|3) => 'Maria')
type K2 = ((a: 1|2|3) => 'Maria') & ((a: number) => string)
declare let fooA:K;
declare let fooB:K2;
fooA = fooB;
fooB = fooA;

passes.

That being said, I think most people would be happy if someone from the TS team would just decide a definition, regardless. As pointed out earlier, the edge cases probably don't matter for most code.

I assume the definition is the major blocker here, or else the TS team doesn't want to do this for other unstated reasons (usefulness?). I'm not sure what trade-offs/shortcomings would be relevant here. Maybe a (another?) use-case would help?

Use case

I'm designing a form widget for type T, where the caller provides metadata for each P in keyof T, the required metadata of a conditional type T[P] extends string ? ....

For string form fields, assigning the initial value of type T[P] in the metadata to the <input>'s value works fine. Reading the value from the <input> value into the constructed object of type T during form submission fails because string is a superset of T[P].

Reversing the conditional to string extends T[P] fails in the opposite order (initial value fails, constructing object succeeds).

T[P] extends string && string extends T[P] doesn't work, syntactically.

The above type Equals<A, B> = ... workarounds do not work in conditional types, I assume also for syntactic reasons.

An operator with the above definition would solve this use case (bi-directional assignment).

Trade-offs, shortcomings

I'm not sure what they'd be. The expected operator == isn't currently allowed, so this wouldn't introduce ambiguity. I can't imagine this would be harder to implement than extends.

@tommy-mitchell
Copy link

tommy-mitchell commented Mar 3, 2023

sindresorhus/type-fest#537 solves identity intersection/equality (TS Playground):

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

Equals<{a: 1} & {a: 1}, {a: 1}>
//=> true, false previously

Equals<{a: 1} | {a: 1}, {a: 1}>
//=> true, false previously

@unional
Copy link
Contributor

unional commented Mar 22, 2023

[email protected] has an implementation that support all of the cases mentioned in here and related issues.

Here are all the tests: https://github.com/unional/type-plus/blob/main/ts/equal/equal.is_equal.spec.ts

@nikelborm
Copy link

UPD @unional: new link https://github.com/unional/type-plus/blob/main/packages/type-plus/src/equal/equal.is_equal.spec.ts

@HansBrende
Copy link
Contributor

HansBrende commented Jan 28, 2025

Another data point: neither Matt's original solution nor type-fest's work for the following test:

type Result = Equals<
//   ^? type Result = false
    `${string & {tag: string}}`,
    `${string & {tag: string}}`
>

As I mentioned in the original issue however, this behavior might also be a typescript regression starting around v5.1.6.

UPDATE: I fixed this bug in #61113, which is now merged.

@louwers
Copy link

louwers commented Apr 7, 2025

@rauschma Found an interesting test case that is broken with most (all?) current type-level equals implementations. Further demonstrating the need for official support for something like this.

#61547

@rauschma
Copy link

rauschma commented Apr 9, 2025

Thinking more about a good utility type Equal<X, Y>:

  • The most intuitive definition of two types being equal is them being mutually assignable.
  • Additionally, any can only be equal to itself – otherwise Equal can’t really be used for testing.

Implementing that is relatively straightforward (vs. hacks requiring you to know the internals of tsc):

type Equal<X, Y> =
  [IsAny<X>, IsAny<Y>] extends [true, true] ? true
  : [IsAny<X>, IsAny<Y>] extends [false, false] ? MutuallyAssignable<X, Y>
  : false
  ;
type IsAny<T> = 0 extends (1 & T) ? true : false;
type MutuallyAssignable<X, Y> =
  [X] extends [Y]
  ? ([Y] extends [X] ? true : false)
  : false
  ;

@HansBrende
Copy link
Contributor

@rauschma @louwers nice find! I had searched for a case where the equals hack was actually less strict than mutual assignability, but hadn't been able to find one until now.

This modification should fix it though:

type MaxInvariance<X> = <T>() => T extends X ? (_: X) => X : 0

(at least with strictFunctionTypes enabled. Without strictFunctionTypes, I think you'd need to do two checks.)

Aside: the bug I mentioned above is now fixed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests