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 35 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
|
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:
|
|
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 |
5a40423 to
6497043
Compare
|
I've reopen this PR. I decided to re-introduce my definition I'm not entirely sure if this is the ideal solution. It seems the error is triggered by the I've verified this approach also resolves the circular reference issue. // source/is-equal.d.ts
type _IsEqual<A, B> =
(<G>() => G extends A & G | G ? 1 : 2) extends
(<G>() => G extends B & G | G ? 1 : 2)
? true
: false;@som-sm As a side note, the current |
|
Minimal repro: type A = ((value: number) => void) & {foo?: 1};
type B = (value: number) => void;
type C = {
(value: 'a'): 1;
(value: string): 1;
};
type D = (value: string) => 1;On this branch, So this change makes distinct function types compare equal. That also means any utility that short-circuits on |
|
Thanks! Firstly, I added these test cases in I pasted them in 2 separate sections. // Branded Type with Tuple
expectType<false>({} as IsEqual<[0, 1] & {foo?: 1}, [0, 1]>) // circular ref
expectType<true>({} as IsEqual<[0, 1] & {foo?: 1}, [0, 1] & {foo?: 1}>) // circular ref// Branded Type with Primitive
expectType<false>({} as IsEqual<1 & {foo?: 1}, 1>); // not type error
expectType<true>({} as IsEqual<1 & {foo?: 1}, 1 & {foo?: 1}>); // not type error
// Branded Type with Lambda
expectType<false>({} as IsEqual<{bar: 'a'} & {foo?: 1}, {bar: 'a'}>); // not type error
expectType<false>({} as IsEqual<((value: number) => void) & {foo?: 1}, (value: number) => void>); // type error
// Overload
expectType<false>({} as IsEqual<{(value: 'a'): 1; (value: string): 1;}, (value: string) => 1>); // type error
expectType<false>({} as IsEqual<{(value: 'a'): 1; (value: string): 1; key: 'value'}, (value: string) => 1>); // not type error
expectType<false>({} as IsEqual<{(value: 'a'): 1; (value: string): 1; key: 'value'}, {(value: string): 1; key: 'value'}>); // type errorThe first two "Branded Type with Tuple" cases were caused by The second set of cases addresses the points raised in the review (I also added more branded type cases). T extends (...arguments_: any[]) => unknown
? IsNever<keyof T> extends true
? T // For functions with no properties
: HasMultipleCallSignatures<T> extends true
? T
: ((...arguments_: Parameters<T>) => ReturnType<T>) & PartialObjectDeep<T, Options>My next steps will be:
To keep things on track, I'm posting this as an interim report in response to the review. I'll follow up once the two points above are resolved. |
Current: #1338 (comment) -> #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