diff --git a/source/exclude-exactly.d.ts b/source/exclude-exactly.d.ts index 5ab98c4fa..3f7025d9d 100644 --- a/source/exclude-exactly.d.ts +++ b/source/exclude-exactly.d.ts @@ -1,8 +1,75 @@ import type {IsNever} from './is-never.d.ts'; import type {IsAny} from './is-any.d.ts'; -import type {If} from './if.d.ts'; -import type {IsEqual} from './is-equal.d.ts'; -import type {IfNotAnyOrNever} from './internal/type.d.ts'; +import type {UnionMember} from './union-member.d.ts'; + +/** +Returns `never` if the 1st and 2nd arguments are identical. +Returns the 1st argument if they are not. +(Note: There are limitations regarding union/intersection types. See `MatchOrNever` or `_IsEqual` in the `source/is-equal.d.ts` documentation.) + +@example +``` +type A = MatchOrNever; +//=> string | number + +type B = MatchOrNever; +//=> never + +type C = MatchOrNever; +//=> string | number + +type D = MatchOrNever; +//=> string +``` + +This does NOT depend on assignability. + +@example +``` +type RO_0 = MatchOrNever<{readonly a: 0}, {a: 0}>; +//=> {readonly a: 0} + +type RO_1 = MatchOrNever<{a: 0}, {readonly a: 0}>; +//=> {a: 0} +``` + +Special cases for `unknown` and `never`, which can easily break equality in type-level codebases. + +@example +``` +type E = MatchOrNever; +//=> unknown + +type F = MatchOrNever; +//=> never + +type G = MatchOrNever; +//=> never + +type H = MatchOrNever; +//=> never +``` + +Note that this does not recursively treat identical union/intersection types (e.g., `T | T` or `T & T`) as `T`. +For instance, `{a: 0} | {a: 0}` is considered identical to `{a: 0}`, but nested properties are not normalized. + +@example +``` +type IDUnion = MatchOrNever<{a: {b: 0}} | {a: {b: 0}}, {a: {b: 0}}>; +//=> never + +type RecurivelyIDUnion = MatchOrNever<{a: {b: 0} | {b: 0}}, {a: {b: 0}}>; +//=> {a: {b: 0} | {b: 0}} +``` +*/ +type MatchOrNever = + [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 extends A & G | G ? 1 : 2) extends (() => G extends B & G | G ? 1 : 2) + ? never + : A; /** A stricter version of `Exclude` that excludes types only when they are exactly identical. @@ -32,26 +99,27 @@ type TestExcludeExactly3 = ExcludeExactly<{a: string} | {a: string; b: string}, @category Improved Built-in */ -export type ExcludeExactly = - IfNotAnyOrNever< - Union, - _ExcludeExactly, - // If `Union` is `any`, then if `Delete` is `any`, return `never`, else return `Union`. - If, never, Union>, - // If `Union` is `never`, then if `Delete` is `never`, return `never`, else return `Union`. - If, never, Union> - >; - -type _ExcludeExactly = - IfNotAnyOrNever, true, never> - : never] extends [never] ? Union : never - : never, - // If `Delete` is `any` or `never`, then return `Union`, - // because `Union` cannot be `any` or `never` here. - Union, Union - >; +export type ExcludeExactly = + IsAny extends true + ? IsAny extends true + ? never + : UnionU + : IsNever extends true + ? IsNever extends true + ? never + : UnionU + : InternalExcludeExactly; + +type InternalExcludeExactly = + UnionMember extends infer D + ? true extends IsNever + ? UnionU + : InternalExcludeExactly<_ExcludeExactly, _ExcludeExactly> + : never; + +type _ExcludeExactly = + UnionU extends unknown // Only for union distribution. + ? MatchOrNever + : never; export {}; diff --git a/source/internal/type.d.ts b/source/internal/type.d.ts index 377c456cb..36f9766a8 100644 --- a/source/internal/type.d.ts +++ b/source/internal/type.d.ts @@ -3,7 +3,7 @@ import type {IsAny} from '../is-any.d.ts'; import type {IsNever} from '../is-never.d.ts'; import type {Primitive} from '../primitive.d.ts'; import type {UnknownArray} from '../unknown-array.d.ts'; -import type {UnionToIntersection} from '../union-to-intersection.d.ts'; +import type {UnionToTuple} from '../union-to-tuple.d.ts'; /** Matches any primitive, `void`, `Date`, or `RegExp` value. @@ -36,6 +36,17 @@ Needed to handle the case of a single call signature with properties. Multiple call signatures cannot currently be supported due to a TypeScript limitation. @see https://github.com/microsoft/TypeScript/issues/29732 + +NOTE: +Deduplicate identical union/intersection types in overloads beforehand with `UniqueOverload` if you require a strict result. + +@example +``` +type A = HasMultipleCallSignatures>; +//=> true +type B = HasMultipleCallSignatures<{(a: number): 0; (a: string): 1; (a: string): 1}>; +//=> false +``` */ export type HasMultipleCallSignatures unknown> = T extends {(...arguments_: infer A): unknown; (...arguments_: infer B): unknown} @@ -162,4 +173,137 @@ export type IsExactOptionalPropertyTypesEnabled = [(string | undefined)?] extend ? false : true; +/** +In TypeScript, `{a: T}` and `{a: T} | {a: T}` are assignable mutually but automatically simplified. +And it disturbs a calculation of `IsEqual`. + +@example +``` +type NT = _IsEqual<{z: {a: 0}}, {z: {a: 0} | {a: 0}}>; // => false +``` + +`UniqueUnionDeep` is a helper type function: removes a duplicated type and keeps the other types recursively. +But union distribution also works as usual outside of objects. + +@example +``` +type UniqueUnionDeepTest = SimplifyDeep>; +//=> {z: {a: {aa: 0}} | {b: 0}; x: '1'} + +type UniqueUnionDeepKeepDistributionTest = SimplifyDeep>; +//=> {z: {a: 0}; x: '1'} | {z: {a: 0}; x: '2'} + +type UniqueUnionDeepArguments = SimplifyDeep {b: number} | {b: number}>>; +//=> (a: {a: number} | {a: number}) => {b: number} | {b: number} + +type UniqueUnionDeepArgumentsDeep = SimplifyDeep {b: {b: number} | {b: number}}>>; +//=> (a: {a: number}) => {b: {b: number}} +``` +*/ +export type UniqueUnionDeep = + /** Note: Wrapping this with `SimplifyDeep`, `test-d/is-equal.ts` fails in `Branded Type with Tuple`. */ + RecurseUniqueUnionDeep<{r: U}>['r']; + +/** +Note: Wrapping this with `Simplify`, `test-d/exact.ts` fails in "Spec: recursive type with union". +*/ +type InternalUniqueUnionDeep = + {[K in keyof U]: UniqueUnion>}; + +type InternalUniqueOverloadDeep unknown> = + /** `Parametes` and `ReturnType` results are possible to be object or lambda; both should be passed into `UniqueUnionDeep`. */ + HasMultipleCallSignatures> extends true + ? U + : (...args: UniqueUnionDeep> extends infer A extends any[] ? A : never) => (UniqueUnionDeep>); + +type RecurseUniqueUnionDeep = + U extends Record + ? InternalUniqueUnionDeep + : U extends UnknownArray + ? InternalUniqueUnionDeep + : U extends Lambda + /** If `IsNever` returns `true`, `U` is composed only of overloads. */ + ? IsNever extends true + ? InternalUniqueOverloadDeep + /** Separates an object into the record and the overload parts. */ + : UniqueUnionDeep> & UniqueUnionDeep> + : U; + +type UniqueOverload unknown> = + T extends { + (...args_: infer Arguments0): infer Return0; + (...args_: infer Arguments1): infer Return1; + (...args_: infer Arguments2): infer Return2; + (...args_: infer Arguments3): infer Return3; + } + ? DedupeTuple<[(...args_: Arguments0) => Return0, (...args_: Arguments1) => Return1, (...args_: Arguments2) => Return2, (...args_: Arguments3) => Return3]> extends infer TupleOverload extends Lambda[] + ? TupleToOverload + : never + : never; // Unreacheable. + +type TupleToOverload = _TupleToOverload; + +type _TupleToOverload = + T extends [(...args_: infer Arguments0) => infer Return0, (...args_: infer Arguments1) => infer Return1, (...args_: infer Arguments2) => infer Return2, (...args_: infer Arguments3) => infer Return3] + ? {(...args_: Arguments0): Return0; (...args_: Arguments1): Return1; (...args_: Arguments2): Return2; (...args_: Arguments3): Return3} + : T extends [(...args_: infer Arguments0) => infer Return0, (...args_: infer Arguments1) => infer Return1, (...args_: infer Arguments2) => infer Return2] + ? {(...args_: Arguments0): Return0; (...args_: Arguments1): Return1; (...args_: Arguments2): Return2} + : T extends [(...args_: infer Arguments0) => infer Return0, (...args_: infer Arguments1) => infer Return1] + ? {(...args_: Arguments0): Return0; (...args_: Arguments1): Return1} + : T extends [(...args_: infer Arguments0) => infer Return0] + ? (...args_: Arguments0) => Return0 + : never; + +/** +@example +``` +type DedupeTuple_Test0 = DedupeTuple<[]>; +//=> [] +type DedupeTuple_Test1 = DedupeTuple<[0, 1]>; +//=> [0, 1] +type DedupeTuple_Test2 = DedupeTuple<[0, 1, number]>; +//=> [0, 1, number] +type DedupeTuple_Test3 = DedupeTuple<[number, 0, 1, number]>; +//=> [number, 0, 1] +type DedupeTuple_Test4 = DedupeTuple<[never, string, '9']>; +//=> [never, string, '9'] +type DedupeTuple_Test5 = DedupeTuple<[string, never, string, '9']>; +//=> [string, never, '9'] +type DedupeTuple_Test6 = DedupeTuple<[unknown, string, '9']>; +//=> [unknown, string, '9'] +type DedupeTuple_Test7 = DedupeTuple<[string, unknown, string, '9']>; +//=> [string, unknown, '9'] +``` +*/ +export type DedupeTuple = _DedupeTuple; + +type _DedupeTuple = + Tuple['length'] extends 0 + ? Return + : Tuple extends [infer Head, ...infer Tail] + ? IsNever extends true + ? _DedupeTuple + : [Compare extends unknown // Union Distribution. + ? _IsEqual<{_compare: Head}, Compare> + : never] extends [false] + ? _DedupeTuple + : _DedupeTuple + : never; // Unreacheable. + +type _IsEqual = + (() => G extends A & G | G ? 1 : 2) extends + (() => G extends B & G | G ? 1 : 2) + ? true + : false; + +type Lambda = ((...args: any[]) => any); + +/** +The flat version of `UniqueUnionDeep`. +*/ +export type UniqueUnion = + UnionToTuple extends infer E extends readonly unknown[] // Improve performance. + ? E[number] + : never; // Unreachable. + export {}; diff --git a/source/is-equal.d.ts b/source/is-equal.d.ts index 306b4687e..442164aa4 100644 --- a/source/is-equal.d.ts +++ b/source/is-equal.d.ts @@ -1,3 +1,5 @@ +import type {UniqueUnionDeep} from './internal/type.d.ts'; + /** Returns a boolean for whether the two given types are equal. @@ -25,13 +27,24 @@ type Includes = @category Utilities */ export type IsEqual = - [A] extends [B] - ? [B] extends [A] - ? _IsEqual - : false + [A, B] extends [B, A] + ? _IsEqual, UniqueUnionDeep> : false; -// This version fails the `equalWrappedTupleIntersectionToBeNeverAndNeverExpanded` test in `test-d/is-equal.ts`. +/** +Note that this doesn't regard the identical union/intersection type `T | T` and/or `T & T` as `T` recursively. +e.g., `{a: 0} | {a: 0}` and/or `{a: 0} & {a: 0}` as `{a: 0}`. + +@example +``` +type IDUnionIsTrue = _IsEqual<{a: {b: 0}} | {a: {b: 0}}, {a: {b: 0}}>; +//=> true +type RecurivelyIDUnionIsFalse = _IsEqual<{a: {b: 0} | {b: 0}}, {a: {b: 0}}>; +//=> false +``` + +This version fails the `equalWrappedTupleIntersectionToBeNeverAndNeverExpanded` test in `test-d/is-equal.ts`. +*/ type _IsEqual = (() => G extends A & G | G ? 1 : 2) extends (() => G extends B & G | G ? 1 : 2) diff --git a/test-d/internal/unique-union-deep.ts b/test-d/internal/unique-union-deep.ts new file mode 100644 index 000000000..836eb2b61 --- /dev/null +++ b/test-d/internal/unique-union-deep.ts @@ -0,0 +1,38 @@ +import {expectType} from 'tsd'; +import type {UniqueUnionDeep} from '../../source/internal/type.d.ts'; + +// The returns of `UniqueUnionDeep` are expected to be equal to `UniqueUnion`, if there's no recursive object type in the passed union type. +// That's why those tests are pasted from 'test-d/internal/union-unique.ts'. + +expectType({} as UniqueUnionDeep); +expectType({} as UniqueUnionDeep); +expectType<{a: 0}>({} as UniqueUnionDeep<{a: 0}>); +expectType({} as UniqueUnionDeep); +expectType({} as UniqueUnionDeep); +expectType<[unknown]>({} as UniqueUnionDeep<[unknown]>); +expectType<[any]>({} as UniqueUnionDeep<[any]>); + +expectType<{a: 0} | {a: 1}>({} as UniqueUnionDeep<{a: 0} | {a: 0} | {a: 0} | {a: 1}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents + +// `{a: t}` isn't excluded by `{a: T}` even if `T` includes `t`. +expectType<{a: 0} | {a: 1} | {a: number}>({} as UniqueUnionDeep<{a: 0} | {a: 0} | {a: 0} | {a: 1} | {a: number}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents + +// `readonly`, `optional`, both, and general object key shouldn't be mutually equal. +expectType<{a: number} | {readonly a: number} | {a?: number} | {readonly a?: number}>({} as UniqueUnionDeep<{a: number} | {readonly a: number} | {a?: number} | {readonly a?: number} | {a: number} | {readonly a: number} | {a?: number} | {readonly a?: number}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents + +// Empty tuple isn't removed by `T[]`. +expectType<[0, 1, 2] | [0, 1] | [] | number[]>({} as UniqueUnionDeep<[0, 1, 2] | [0, 1, 2] | [0, 1, 2] | [0, 1] | [] | number[]>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents + +// `T[]` doesn't delete tuples of `[t, ...t]` even if `T` includes `t`. +expectType<[0, 1, 2] | ['0', unknown] | [0, unknown]>({} as UniqueUnionDeep<[0, 1, 2] | [0, 1, 2] | [0, 1, 2] | [0, unknown] | ['0', unknown]>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents + +// `[u, ...u]` doesn't delete tuple of `[v, ...y]` even if `u` includes `v`. +expectType<[0, 1, 2] | ['0', unknown] | [0, unknown] | number[]>({} as UniqueUnionDeep<[0, 1, 2] | [0, 1, 2] | [0, 1, 2] | [0, unknown] | ['0', unknown] | number[]>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents + +expectType<{z: {a: {aa: 0}} | {b: 0}; x: '1'}>({} as UniqueUnionDeep<{z: {a: {aa: 0} | {aa: 0}} | {a: {aa: 0} | {aa: 0}} | {b: 0}; x: '1'}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +expectType<{z: {a: 0}; x: '1'} | {z: {a: 0}; x: '2'}>({} as UniqueUnionDeep<{z: {a: 0} | {a: 0}; x: '1'} | {z: {a: 0} | {a: 0}; x: '2'}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents + +type UniqueUnionDeepArgumentsDeep = UniqueUnionDeep<(a: {a: number} | {a: number}) => {b: {b: number} | {b: number}}>; // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +expectType<(a: {a: number}) => {b: {b: number}}>({} as UniqueUnionDeepArgumentsDeep); + +expectType<{a: [{b: 0}]}>({} as UniqueUnionDeep<{a: [{b: 0} | {b: 0}]} | {a: [{b: 0}]}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents diff --git a/test-d/internal/unique-union.ts b/test-d/internal/unique-union.ts new file mode 100644 index 000000000..eecf9af05 --- /dev/null +++ b/test-d/internal/unique-union.ts @@ -0,0 +1,27 @@ +import {expectType} from 'tsd'; +import type {UniqueUnion} from '../../source/internal/type.d.ts'; + +expectType({} as UniqueUnion); +expectType({} as UniqueUnion); +expectType<{a: 0}>({} as UniqueUnion<{a: 0}>); +expectType({} as UniqueUnion); +expectType({} as UniqueUnion); +expectType<[unknown]>({} as UniqueUnion<[unknown]>); +expectType<[any]>({} as UniqueUnion<[any]>); + +expectType<{a: 0} | {a: 1}>({} as UniqueUnion<{a: 0} | {a: 0} | {a: 0} | {a: 1}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents + +// `{a: t}` isn't excluded by `{a: T}` even if `T` includes `t`. +expectType<{a: 0} | {a: 1} | {a: number}>({} as UniqueUnion<{a: 0} | {a: 0} | {a: 0} | {a: 1} | {a: number}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents + +// `readonly`, `optional`, both, and general object key shouldn't be mutually equal. +expectType<{a: number} | {readonly a: number} | {a?: number} | {readonly a?: number}>({} as UniqueUnion<{a: number} | {readonly a: number} | {a?: number} | {readonly a?: number} | {a: number} | {readonly a: number} | {a?: number} | {readonly a?: number}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents + +// Empty tuple isn't removed by `T[]`. +expectType<[0, 1, 2] | [0, 1] | [] | number[]>({} as UniqueUnion<[0, 1, 2] | [0, 1, 2] | [0, 1, 2] | [0, 1] | [] | number[]>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents + +// `T[]` doesn't delete tuples of `[t, ...t]` even if `T` includes `t`. +expectType<[0, 1, 2] | ['0', unknown] | [0, unknown]>({} as UniqueUnion<[0, 1, 2] | [0, 1, 2] | [0, 1, 2] | [0, unknown] | ['0', unknown]>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents + +// `[u, ...u]` doesn't delete tuples of `[v, ...y]` even if `u` includes `v`. +expectType<[0, 1, 2] | ['0', unknown] | [0, unknown] | number[]>({} as UniqueUnion<[0, 1, 2] | [0, 1, 2] | [0, 1, 2] | [0, unknown] | ['0', unknown] | number[]>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents diff --git a/test-d/is-equal.ts b/test-d/is-equal.ts index c7856c540..475434d38 100644 --- a/test-d/is-equal.ts +++ b/test-d/is-equal.ts @@ -42,6 +42,122 @@ expectType({} as IsEqual<[string], [string]>); expectType({} as IsEqual<[string], [string, number]>); expectType({} as IsEqual<[0, 1] | [0, 2], [0, 2]>); +// Lambda cases. +type SpecificNumericLambda = (value: {a: 1}) => {b: 1}; +type SpecificNumericLambda1 = (value: {a: 2}) => {b: 1}; +type NumberLambda = (value: {a: number}) => {b: number}; +type AnyLambda = (value: {a: any}) => {b: any}; +type UnknownLambda = (value: {a: unknown}) => {b: unknown}; +type NeverLambda = (value: {a: never}) => {b: never}; +expectType({} as IsEqual); +expectType({} as IsEqual); +expectType({} as IsEqual); +expectType({} as IsEqual); +expectType({} as IsEqual); +expectType({} as IsEqual); +expectType({} as IsEqual); +expectType({} as IsEqual); +expectType({} as IsEqual); +expectType({} as IsEqual); +expectType({} as IsEqual); +expectType({} as IsEqual); +expectType({} as IsEqual); + +// Branded Type with Tuple +expectType({} as IsEqual<[0, 1] & {foo?: 1}, [0, 1]>); +expectType({} as IsEqual<[0, 1] & {foo?: 1}, [0, 1] & {foo?: 1}>); +// Branded Type with Primitive +expectType({} as IsEqual<1 & {foo?: 1}, 1>); +expectType({} as IsEqual<1 & {foo?: 1}, 1 & {foo?: 1}>); +// Branded Type with Lambda +expectType({} as IsEqual<{bar: 'a'} & {foo?: 1}, {bar: 'a'}>); +expectType({} as IsEqual<((value: number) => void) & {foo?: 1}, (value: number) => void>); + +// Overloads +expectType({} as IsEqual<{(value: 'a'): 1; (value: string): 1}, (value: string) => 1>); +expectType({} as IsEqual<{(value: 'a'): 1; (value: string): 1; key: 'value'}, (value: string) => 1>); +expectType({} as IsEqual<{(value: 'a'): 1; (value: string): 1; key: 'value'}, {(value: string): 1; key: 'value'}>); + +declare function testf(a: string): number; +declare function testf(a: number): string; + +declare function testg(a: number): string; +declare function testg(a: string): number; +expectType({} as IsEqual); + +expectType({} as IsEqual<{(a: string): {rr: number}; (a: string): {rr: number}}, (a: string) => {rr: number}>); +expectType({} as IsEqual<{(a: {aa: string} | {aa: string}): {rr: number}; (a: {aa: string}): {rr: number}}, (a: {aa: string}) => ({rr: number} | {rr: number})>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +expectType({} as IsEqual<{(a: {aa: string} & {aa: string}): {rr: number}; (a: {aa: string}): {rr: number}}, (a: {aa: string}) => ({rr: number} & {rr: number})>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +expectType({} as IsEqual<{(a: {aa: string} | {aa: string}): {rr: number}; (a: {aa: string}): {rr: number}}, (a: {aa: string} | {aa: string}) => ({rr: number} | {rr: number})>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +expectType({} as IsEqual<{(a: {aa: string} & {aa: string}): {rr: number}; (a: {aa: string}): {rr: number}}, (a: {aa: string} & {aa: string}) => ({rr: number} & {rr: number})>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents + +// Object: Overload & Record. +expectType({} as IsEqual<{key: 'value'; (a: string): {rr: number}}, {key: 'value'; (a: string): {rr: number}}>); +expectType({} as IsEqual<{key: 'value'; (a: string): {rr: number}; (a: string): {rr: number}}, {key: 'value'; (a: string): {rr: number}}>); + +expectType({} as IsEqual<{(value: 'a'): 1; (value: string): 1; key0: {key1: {key2: 'value2'} | {key2: 'value2'}}}, {(value: string): 1; key0: {key1: {key2: 'value2'} | {key2: 'value2'}}}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +expectType({} as IsEqual<{(value: string): 1; key0: {key1: {key2: 'value2'} | {key2: 'value2'}}}, {(value: string): 1; key0: {key1: {key2: 'value2'} | {key2: 'value2'}}}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +expectType({} as IsEqual<{(value: string): 1; key0: {key1: {key2: 'value2'} | {key2: 'value2'}}}, {(value: string): 1; key0: {key1: {key2: 'value2'}}}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents + +// Lambda: Identical Union and Intersection cases. +expectType({} as IsEqual); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +expectType({} as IsEqual); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +type NumberLambdaIntersection = (value: {a: number} & {a: number}) => {b: number}; // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +type NumberLambdaUnion = (value: {a: number} | {a: number}) => {b: number}; // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +type NumberLambdaReturnIntersection = (value: {a: number}) => {b: number} & {b: number}; // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +type NumberLambdaReturnUnion = (value: {a: number}) => {b: number} | {b: number}; // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +type NumberLambdaBothIntersection = (value: {a: number} & {a: number}) => {b: number} & {b: number}; // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +type NumberLambdaBothUnion = (value: {a: number} | {a: number}) => {b: number} | {b: number}; // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents + +expectType({} as IsEqual); +expectType({} as IsEqual); +expectType({} as IsEqual); +expectType({} as IsEqual); +expectType({} as IsEqual); +expectType({} as IsEqual); +expectType({} as IsEqual); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +expectType({} as IsEqual); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +expectType({} as IsEqual); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +expectType({} as IsEqual); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +expectType({} as IsEqual); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +expectType({} as IsEqual); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents + +// Date cases. +const foo = {date: new Date(), a: null} as const; +expectType({} as IsEqual); +expectType({} as IsEqual<{a: null}, typeof foo>); +expectType({} as IsEqual<{date: Date}, typeof foo>); +// Date: Identical Union and Intersection cases. + +// Set cases. +expectType({} as IsEqual, Set>); +expectType({} as IsEqual, Set>); +expectType({} as IsEqual, Set>); + +// Set: Identical Union and Intersection cases. +expectType({} as IsEqual<{a: {b: Set<0> & Set<0>}}, {a: {b: Set<0>}}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +expectType({} as IsEqual<{a: {b: Set<0> | Set<0>}}, {a: {b: Set<0>}}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents + +// Set: Identical Union and Intersection cases. +expectType({} as IsEqual<{a: {b: Set<0> & Set<0>}}, {a: {b: Set<0>}}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +expectType({} as IsEqual<{a: {b: Set<0> | Set<0>}}, {a: {b: Set<0>}}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents + +// Map cases. +expectType({} as IsEqual, Map>); +expectType({} as IsEqual, Map>); +expectType({} as IsEqual, Map>); +expectType({} as IsEqual, Map>); +expectType({} as IsEqual, Map>); +expectType({} as IsEqual, Map>); +expectType({} as IsEqual, Map>); +expectType({} as IsEqual, Map>); +expectType({} as IsEqual, Map>); +expectType({} as IsEqual, Map>); + +// Map: Identical Union and Intersection cases. +expectType({} as IsEqual | Map, Map>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +expectType({} as IsEqual & Map, Map>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents + type LongTupleNumber = TupleOf<50, 0>; expectType({} as IsEqual); @@ -89,3 +205,38 @@ expectType(equalTupleIntersectionAndTuple); // Test for Issue https://github.com/sindresorhus/type-fest/issues/1305 type Assignability> = any; type TestAssignability = Assignability; + +// Ensure `{a: t; b: s}` === `{a: t} & {b: s}`, not equal to `{a: u} & {b: v}` if `u` !== `t` or `v` !== `s`. +expectType({} as IsEqual<{a: 0} & {b: 0}, {a: 0; b: 0}>); +expectType({} as IsEqual<{aa: {a: {x: 0} & {y: 0}} & {b: 0}}, {aa: {a: {x: 0; y: 0}; b: 0}}>); +expectType({} as IsEqual<{a: 1} & {b: 0}, {a: 0; b: 0}>); +expectType({} as IsEqual<{aa: {a: {x: 1} & {y: 0}} & {b: 0}}, {aa: {a: {x: 0; y: 0}; b: 0}}>); + +// Ensure `{a: t} | {a: t}` === `{a: t}` +expectType({} as IsEqual<{a: 0} & ({b: 0} | {b: 0}), {a: 0; b: 0}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +expectType({} 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-constituents +expectType({} as IsEqual<{readonly a: 0} & ({b: 0} | {b: 0}), {a: 0; b: 0}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +expectType({} as IsEqual<{readonly aa: {a: 0} & ({b: 0} | {b: 0})}, {aa: {a: 0; b: 0}}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents + +// `readonly key` should not be equal to `key` whether recursively or not. +expectType({} as IsEqual<{readonly a: 0} & {b: 0}, {a: 0; b: 0}>); +expectType({} as IsEqual<{readonly aa: {a: 0} & {b: 0}}, {aa: {a: 0; b: 0}}>); +expectType({} 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({} 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 + +// Assume that two lambdas `Parameters` and `ReturnType` are equal, `IsEqual` returns `true`. +type ArgumentsExpected = (a: {a: number}) => {b: number}; +type ArgumentsIdenticalUnion = (a: {a: number} | {a: number}) => ({b: number} | {b: number}); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +expectType({} as IsEqual); +type ArgumentsIdenticalIntersection = (a: {a: number} & {a: number}) => ({b: number} & {b: number}); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +expectType({} as IsEqual {b: number}>); +type ArgumentsDeepExpected = (a: {a: {b: number}}) => {b: {b: number}}; +type ArgumentsIdenticalUnionDeep = (a: {a: {b: number} | {b: number}}) => {b: {b: number} | {b: number}}; // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +expectType({} as IsEqual); +type ArgumentsIdenticalIntersectionDeep = (a: {a: {b: number} & {b: number}}) => {b: {b: number} & {b: number}}; // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +expectType({} as IsEqual); + +// Deep identical union/intersection in tuples. +type TupleDeepIdenticalExpected = {a: [{b: 0}]}; +expectType({} as IsEqual<{a: [{b: 0} | {b: 0}]} | {a: [{b: 0}]}, TupleDeepIdenticalExpected>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +expectType({} as IsEqual<{a: [{b: 0} & {b: 0}]} | {a: [{b: 0}]}, TupleDeepIdenticalExpected>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents