-
Notifications
You must be signed in to change notification settings - Fork 12.8k
[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
Comments
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. |
@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)? |
👀 These types are very much different; |
oops, updated with a better example |
|
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 |
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 |
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 |
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 |
I understand your point of view. I agree that the false2 should be true. so i corrected it. |
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 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? |
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" |
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
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
fails as expected.
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 caseI'm designing a form widget for type For Reversing the conditional to
The above An operator with the above definition would solve this use case (bi-directional assignment). Trade-offs, shortcomingsI'm not sure what they'd be. The expected operator |
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 |
[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 |
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. |
Thinking more about a good utility type
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
;
|
@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 Aside: the bug I mentioned above is now fixed. |
original issue: #27024
workarounds
to summarize the discussion in #27024, the accepted solution was:
however there are many edge cases where it fails:
{}
types - [Feature request]type level equal operator #27024 (comment)Equal<{ x: 1 } & { y: 2 }, { x: 1, y: 2 }>
- [Feature request]type level equal operator #27024 (comment)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 sameThe text was updated successfully, but these errors were encountered: