fix: IsEqual, {a: t, b: s} and {a: t} & {b: s} are equal#1338
fix: IsEqual, {a: t, b: s} and {a: t} & {b: s} are equal#1338taiyakihitotsu wants to merge 30 commits intosindresorhus:mainfrom
IsEqual, {a: t, b: s} and {a: t} & {b: s} are equal#1338Conversation
|
Reopened. |
|
I've finally fixed it. Now I also removed the test case (commited in dabe872) below because the four new test cases in this PR already cover it. (Please let me know if I missed anything.) // Distinct whether an object is merged by `&` or via `Simplify`, to ensure Branded Types are handled strictly.
export type IntersectionMerge<tuple extends readonly unknown[]> = Except<tuple, 'length'> & {__brand: 'tag'};
type SampleTuple = [0, 1, 2];
expectType<true>({} as IsEqual<{a: 0} & {b: 0}, {a: 0; b: 0}>);
expectType<true>({} as IsEqual<Simplify<IntersectionMerge<SampleTuple>>, Merge<IntersectionMerge<SampleTuple>, IntersectionMerge<SampleTuple>>>);Thanks! |
…urns `true` with intersection object and expanded object
|
I found this case failed even if just using expectType<true>({} as IsEqual<{aa: {a: {x: 0} & ({y: 0} | {y: 0})} & {b: 0}}, {aa: {a: {x: 0; y: 0}; b: 0}}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituentsexport type IsEqual<A, B> =
[A, B] extends [B, A]
? [A, B] extends [object, object]
? _IsEqual<SimplifyDeep<A>, SimplifyDeep<B>>
: _IsEqual<A, B>
: false;For example, this is a simple case. type NT = _IsEqual<{z: {a: 0}}, {z: {a: 0} | {a: 0}}>; // => falseUsing To remove duplicated types in union, I defined Please let me know if I've missed any cases. |
|
After a sleep, I've found another error in the definition of export type UniqueUnion<U> = _UniqueUnion<U, never>;
type _UniqueUnion<U, R> =
LastOfUnion<U> extends infer K
? [K] extends [never]
? R
: _UniqueUnion<Exclude<U, K>, K extends R ? R : R | K>
: never;
_UniqueUnion<Exclude<U, K>, K extends R ? R : R | K>
Reopened. |
StatusI've added 5 helper type functions in
This PR passes those test cases (which fails before this PR): // Ensure `{a: t; b: s}` === `{a: t} & {b: s}`, not equal to `{a: u} & {b: v}` if `u` !== `t` or `v` !== `s`.
expectType<true>({} as IsEqual<{a: 0} & {b: 0}, {a: 0; b: 0}>);
expectType<true>({} as IsEqual<{aa: {a: {x: 0} & {y: 0}} & {b: 0}}, {aa: {a: {x: 0; y: 0}; b: 0}}>);
// Ensure `{a: t} | {a: t}` === `{a: t}`
expectType<true>({} as IsEqual<{a: 0} & ({b: 0} | {b: 0}), {a: 0; b: 0}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents
expectType<true>({} as IsEqual<{aa: {a: {x: 0} & ({y: 0} | {y: 0})} & {b: 0}}, {aa: {a: {x: 0; y: 0}; b: 0}}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituentsI've submitted this to fix BackgroundI found the bug described above, where unions inside recursive objects cannot be compared directly by the previous To resolve this, I implemented export type UniqueUnion<U> = _UniqueUnion<U, never>;
type _UniqueUnion<U, R> =
LastOfUnion<U> extends infer K
? [K] extends [never]
? R
: _UniqueUnion<
Exclude<U, K>, // (1)
(<G>() => G extends K & G | G ? 1 : 2) extends
(<G>() => G extends R & G | G ? 1 : 2)
? [R, unknown] extends [never, K]
? K
: R
: R | K>
: never;I think this is the idiomatic way to iterate over union types, but it didn't work as expected.
So I decided to define And I added the helper function: export type MatchOrNever<A, B> =
[unknown, B] extends [A, never]
? A
// This equality code base below doesn't work if `A` is `unknown` and `B` is `never` case.
// So this branch should be wrapped to take care of this.
: (<G>() => G extends A & G | G ? 1 : 2) extends (<G>() => G extends B & G | G ? 1 : 2)
? never
: A;I've finished creating the strict export type IsEqual<A, B> =
[A, B] extends [B, A]
? [A, B] extends [object, object]
? _IsEqual<SimplifyDeep<UniqueUnionDeep<A>>, SimplifyDeep<UniqueUnionDeep<B>>>
: _IsEqual<A, B>
: false;PR for
|
…2nd argument is a union type
6bac156 to
9af5c71
Compare
|
Reopen. |
1340a5d to
2f1d991
Compare
source/is-equal.d.ts
Outdated
| : false | ||
| [A, B] extends [B, A] | ||
| ? [A, B] extends [object, object] | ||
| ? _IsEqual<SimplifyDeep<UniqueUnionDeep<A>>, SimplifyDeep<UniqueUnionDeep<B>>> |
There was a problem hiding this comment.
@taiyakihitotsu Not sure if want to make IsEqual so complicated just to make {a: t, b: s} and {a: t} & {b: s} equal. Isn't there a simpler way of achieving this? And maybe in the future, this might just work automatically, refer microsoft/TypeScript#60726.
There was a problem hiding this comment.
@som-sm
I've not found an elegant solution to unify a union type...
And maybe in the future, this might just work automatically
Thanks for your information!
I hope it to be merged, but I think we should not wait for it because the last comment was posted a month ago, no reviews.
We cannot predict when and (even) whether it will be merged.
I believe we should close any potential bugs which we can.
Even if this PR is merged and results in redundant logic in IsEqual, I expect type safety will not be compromised.
(But, if merged, a separate PR for performance optimization would likely be required later.)
I've done a refactor for UniqueUnion with UnionToTuple.
type UniqueUnion<U> = UnionToTuple<U>[number]Simple.
But in this naive definition, npm test spits a stack-overflow.
✔ lint-rules/validate-jsdoc-codeblocks.test.js (11785.496384ms)
ℹ tests 37
ℹ suites 1
ℹ pass 37
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 11849.291703
<--- Last few GCs --->
[25259:0x556c1abd6000] 64324 ms: Scavenge 6096.5 (6140.3) -> 6084.4 (6141.8) MB, pooled: 0 MB, 4.60 / 0.00 ms (average mu = 0.359, current mu = 0.354) allocation failure;
[25259:0x556c1abd6000] 65764 ms: Mark-Compact (reduce) 6110.4 (6154.5) -> 6063.4 (6113.3) MB, pooled: 0 MB, 47.19 / 0.00 ms (+ 1233.1 ms in 247 steps since start of marking, biggest step 5.2 ms, walltime since start of marking 1416 ms) (average mu = 0
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
----- Native stack trace -----
1: 0x556bfb9bddd9 node::OOMErrorHandler(char const*, v8::OOMDetails const&) [node]
2: 0x556bfbe20da4 [node]
3: 0x556bfbe20e89 [node]
4: 0x556bfc0a91fb [node]
5: 0x556bfc0a92f2 [node]
6: 0x556bfc0a93c5 [node]
7: 0x556bfc0b8cec [node]
8: 0x556bfc0bbcc5 [node]
9: 0x556bfcb0de17 [node]
ERROR: "test:tsc" exited with 134.
I found it was caused by the definition of UniqueUnion and UnionToTuple.
- In
UniqueUnion, a result ofUnionToTupleshould be stored withinfer. - In
UnionToTuple, a result ofExcludeExactly<T, L>before expanding it.
export type UnionToTuple<T, L = LastOfUnion<T>> =
IsNever<T> extends false
? ExcludeExactly<T, L> extends infer E // Improve performance.
? [...UnionToTuple<E>, L]
: never // Unreachable.
: [];Implementing either of these is enough to fix the stack overflow though, I’ve applied both just to be safe.
There was a problem hiding this comment.
I believe we should close any potential bugs which we can.
Yeah true, I just linked that PR so that in future when that gets merged we might be able to revert the changes done here.
Let me give this a thorough read sometime next week. I'll also go through all your previous attempts and the issues that existed in those.
In the meanwhile, feel free to open PRs for:
- Exposing
LastOfUnion. - Adding
ExcludeExactly. - Fixing the
UnionToTuplebug.
These should be easier to review and get merged.
|
See this PR: #1349 And I found another bug in type DifferentModifiers = {a: 0} | {readonly a: 0} | {a?: 0} | {readonly a?: 0};
expectType<UnionToTuple<DifferentModifiers>[number]>({} as DifferentModifiers);Expected this to pass, but
To separate this point from this PR, And we should avoid duplicate definitions. So I will make a separate PR for it, including:
|
| expectType<false>({} as IsEqual<{readonly a: 0} & {b: 0}, {a: 0; b: 0}>); | ||
| expectType<false>({} as IsEqual<{readonly aa: {a: 0} & {b: 0}}, {aa: {a: 0; b: 0}}>); | ||
| expectType<false>({} as IsEqual<{readonly aa: {a: 0} & {b: 0} | {a: 0} & {b: 0}}, {aa: {a: 0; b: 0}}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents | ||
| expectType<false>({} as IsEqual<{aa: {a: 0} & {b: 0} | {a: 0} & {b: 0}}, {aa: {readonly a: 0; b: 0}}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents |
There was a problem hiding this comment.
A couple of more tests. Maybe add around line 34:
expectType<true>({} as IsEqual<(value: {a: 1}) => {b: 1}, (value: {a: 1}) => {b: 1}>);
expectType<false>({} as IsEqual<(value: {a: 1}) => {b: 1}, (value: {a: 2}) => {b: 1}>);|
Thank you! I’ve left While adding the tests you suggested, I discovered a similar issue about lambda arguments and included an update for it as well. |
|
Found another edge case where lambda arguments and return types are identical unions or intersections. This isn't a regression; the previous import {expectType} from 'tsd';
type NumberLambda = (value: {a: number}) => {b: number};
type NumberLambdaIntersection = (value: {a: number} & {a: number}) => {b: number};
type NumberLambdaUnion = (value: {a: number} | {a: number}) => {b: number};
type NumberLambdaReturnIntersection = (value: {a: number}) => {b: number} & {b: number};
type NumberLambdaReturnUnion = (value: {a: number}) => {b: number} | {b: number};
type _IsEqual<A, B> =
(<G>() => G extends A & G | G ? 1 : 2) extends
(<G>() => G extends B & G | G ? 1 : 2)
? true
: false;
expectType<true>({} as _IsEqual<NumberLambdaIntersection, NumberLambda>);
//=> error
expectType<true>({} as _IsEqual<NumberLambdaUnion, NumberLambda>);
//=> error
expectType<true>({} as _IsEqual<NumberLambdaReturnIntersection, NumberLambda>);
//=> error
expectType<true>({} as _IsEqual<NumberLambdaReturnUnion, NumberLambda>);
//=> error |
Current: #1338 (comment) -> #1338 (comment) -> #1338 (comment) -> #1338 (comment)
This PR is separated from #1336.
(See #1336 (comment))
Major Changes
The previous (meaning before this PR)
IsEqual<{a: 0} & {b: 0}, {a: 0; b: 0}>returnsfalse.This PR prevents it to return
trueviaSimplifySimplifyDeepif both areextends object.(intersection of objects, it's deprecated though.
https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html#intersection-types)
Minor Changes
Refactor: delete unused
import