Skip to content
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
dabe872
fix: `IsEqual`, `{a: t, b: s}` and `{a: t} & {b: s}` are equal
taiyakihitotsu Jan 27, 2026
74b0e71
Fix: `IsEqual`, returns `true` for nested intersection of objects
taiyakihitotsu Jan 27, 2026
4c9ef58
Merge branch 'main' into fix/is-equal-20260127
taiyakihitotsu Jan 27, 2026
b887c8c
refactor: remoe unused imports
taiyakihitotsu Jan 27, 2026
df808ce
refactor: remove unused import
taiyakihitotsu Jan 27, 2026
b213bb7
revert: `test-d/is-equal.ts`
taiyakihitotsu Jan 27, 2026
4d8773c
fix: define `UniqueUnionDeep` and `UniqueUnion`, to fix `IsEqual` ret…
taiyakihitotsu Jan 27, 2026
fb14a98
doc: update `UniqueUnionDeep` comment
taiyakihitotsu Jan 27, 2026
7f0162f
doc: update `test-d/is-equal.ts` comment
taiyakihitotsu Jan 27, 2026
ba7ccab
test: add tests
taiyakihitotsu Jan 28, 2026
2cc3a26
update: add `UniqueUnionDeep` to define `IsEqual` to strictly get `A|…
taiyakihitotsu Jan 28, 2026
590587c
fix: rename `ExcludeExactly` from `UniqueExclude`, update for a case …
taiyakihitotsu Feb 1, 2026
9af5c71
Merge branch 'main' into fix/is-equal-20260127
taiyakihitotsu Feb 1, 2026
2f1d991
build: trigger CI
taiyakihitotsu Feb 3, 2026
d02b353
revert `IsEqual` and `Paths` changes
som-sm Feb 4, 2026
c6d4206
revert: re-revert d02b353, `source/is-equal.d.ts` and `test-d/is-equa…
taiyakihitotsu Feb 4, 2026
687e6ed
add: `LastOfUnion`, return a type of an union-type (order is not guar…
taiyakihitotsu Feb 5, 2026
631a49f
refactor: `UnionToTuple`, import `LastOfUnion` instead of define
taiyakihitotsu Feb 5, 2026
755df63
Add `ExcludeExactly`, distinguish between different modifiers.
taiyakihitotsu Feb 5, 2026
1f76fd5
fix: `UnionToTuple`, use `ExcludeExactly`, improve performance.
taiyakihitotsu Feb 5, 2026
7d990be
Refactor: `UniqueUnion`, use `UnionToTuple`, improve performance.
taiyakihitotsu Feb 5, 2026
521c373
Merge branch 'main' into fix/is-equal-20260127
taiyakihitotsu Feb 5, 2026
944a74b
temp: ignore arrow-doc.
taiyakihitotsu Feb 5, 2026
a8c2695
refactor: `IsEqual`, remove redundant `SimplifyDeep`, fix the doc.
taiyakihitotsu Feb 5, 2026
390e127
Merge branch 'main' into fix/is-equal-20260127
taiyakihitotsu Feb 7, 2026
18eb4af
Revert "temp: ignore arrow-doc."
taiyakihitotsu Feb 7, 2026
4282283
Update readme.md
sindresorhus Feb 10, 2026
c5ade48
update: `UniqueUnionDeep`, add `IsEqual`'s lambda test cases
taiyakihitotsu Feb 10, 2026
5ad2c4d
update: `UniqueUnionDeep` handles records in tuples.
taiyakihitotsu Feb 12, 2026
9d6aa80
test: add deep identical cases for `IsEqual` and `UniqueUnionDeep`
taiyakihitotsu Feb 12, 2026
946b90f
Merge branch 'main' into fix/is-equal-20260127
taiyakihitotsu Mar 20, 2026
6497043
fix: revert `IsEqual` and the tests. fix `ExcludeExactly` to improve …
taiyakihitotsu Mar 20, 2026
8a7056b
refactor: rename `SimplifyUniqueUnionDeep` to `InternalUniqueUnionDeep`
taiyakihitotsu Mar 21, 2026
4f490d2
fix: Overload cases
taiyakihitotsu Mar 22, 2026
eb8828b
fix: Branded Type with Tuple
taiyakihitotsu Mar 22, 2026
0298dad
update: handles overload cases
taiyakihitotsu Jun 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ export type {IsNullable} from './source/is-nullable.d.ts';
export type {TupleOf} from './source/tuple-of.d.ts';
export type {ExclusifyUnion} from './source/exclusify-union.d.ts';
export type {ArrayReverse} from './source/array-reverse.d.ts';
export type {LastOfUnion} from './source/last-of-union.d.ts';

// Template literal types
export type {CamelCase, CamelCaseOptions} from './source/camel-case.d.ts';
Expand Down Expand Up @@ -207,5 +208,6 @@ export type {TsConfigJson} from './source/tsconfig-json.d.ts';
export type {ExtendsStrict} from './source/extends-strict.d.ts';
export type {ExtractStrict} from './source/extract-strict.d.ts';
export type {ExcludeStrict} from './source/exclude-strict.d.ts';
export type {ExcludeExactly} from './source/exclude-exactly.d.ts';

export {};
2 changes: 2 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ Click the type names for complete docs.
- [`ConditionalSimplify`](source/conditional-simplify.d.ts) - Simplifies a type while including and/or excluding certain types from being simplified.
- [`ConditionalSimplifyDeep`](source/conditional-simplify-deep.d.ts) - Recursively simplifies a type while including and/or excluding certain types from being simplified.
- [`ExclusifyUnion`](source/exclusify-union.d.ts) - Ensure mutual exclusivity in object unions by adding other members’ keys as `?: never`.
- [`LastOfUnion`](source/last-of-union.d.ts) - Return a type picked in an union-type. Order is not guaranteed.

### Type Guard

Expand Down Expand Up @@ -319,6 +320,7 @@ Click the type names for complete docs.
- [`ExtendsStrict`](source/extends-strict.d.ts) - A stricter, non-distributive version of `extends` for checking whether one type is assignable to another.
- [`ExtractStrict`](source/extract-strict.d.ts) - A stricter version of `Extract<T, U>` that ensures every member of `U` can successfully extract something from `T`.
- [`ExcludeStrict`](source/exclude-strict.d.ts) - A stricter version of `Exclude<T, U>` that ensures every member of `U` can successfully exclude something from `T`.
- [`ExcludeExactly`](source/exclude-exactly.d.ts) - A stricter version of `Exclude<T, U>` that ensures objects with different key modifiers are not considered identical.

## Declined types

Expand Down
98 changes: 98 additions & 0 deletions source/exclude-exactly.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type {IsUnknown} from './is-unknown.d.ts';
import type {IsNever} from './is-never.d.ts';
import type {IsAny} from './is-any.d.ts';
import type {LastOfUnion} from './last-of-union.d.ts';

/**
Return `never` if the 1st and the 2nd arguments are mutually identical.
Return the 1st if not.
(But there's a limitation about union/intersection type. See `MatchOrNever` or `_IsEqual` in `source/is-equal.d.ts` doc.)

@example
```
type A = MatchOrNever<string | number, string>; // => string | number
type B = MatchOrNever<string | number, string | number>; // => never
type C = MatchOrNever<string | number, unknown>; // => string | number
type D = MatchOrNever<string, string | number>; // => 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}
```

`unknown` and `never` cases, which easily break equality in type level code base.

@example
```
type E = MatchOrNever<unknown, never>; // => unknown
type F = MatchOrNever<unknown, unknown>; // => never
type G = MatchOrNever<never, never>; // => never
type H = MatchOrNever<never, unknown>; // => never
```

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 IDUnion = MatchOrNever<{a: {b: 0}} | {a: {b: 0}}, {a: {b: 0}}>; // => never
type A = {a: {b: 0} | {b: 0}};
type RecurivelyIDUnion = MatchOrNever<A, {a: {b: 0}}>; // => A
```
*/
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;

/**
TypeScript's built-in `Exclude` and `ExcludeStrict` in `type-fest` don't distinguish kinds of keys of objects.

@example
```
import type {ExcludeStrict} from 'type-fest';

type NeverReturned_0 = Exclude<{a: 0} | {readonly a: 0}, {readonly a: 0}>; // => never
type NeverReturned_1 = ExcludeStrict<{a: 0} | {readonly a: 0}, {readonly a: 0}>; // => never
```

This `ExcludeExactly` keeps the union objects element if the keys of the first and the second aren't identical.

@example
```
import type {ExcludeExactly} from 'type-fest';

type ExcludeNever = ExcludeExactly<{a: 0} | {a: 0} | {readonly a: 0}, never>; // => {a: 0} | {a: 0} | {readonly a: 0}
Comment thread
taiyakihitotsu marked this conversation as resolved.
Outdated
type ExcludeReadonlyKey = ExcludeExactly<{a: 0} | {readonly a: 0}, {readonly a: 0}>; // => {a: 0}
type ExcludeKey = ExcludeExactly<{readonly a: 0}, {a: 0}>; // => {readonly a: 0}
type ExcludeReadonly = ExcludeExactly<{readonly a: 0}, {readonly a: 0}>; // => {readonly a: 0}
Comment thread
taiyakihitotsu marked this conversation as resolved.
Outdated
type ExcludeSubType = ExcludeExactly<0 | 1 | number, 1>; // => number
type ExcludeAllSet = ExcludeExactly<0 | 1 | number, number>; // => never
type ExcludeFromUnknown = ExcludeExactly<unknown, string>; // => unknown
type ExcludeFromUnknownArray = ExcludeExactly<number[] | unknown[], number[]>; // => unknown[]
```
*/
export type ExcludeExactly<UnionU, DeleteT> =
LastOfUnion<DeleteT> extends infer D
? true extends IsNever<D>
? UnionU
: ExcludeExactly<_ExcludeExactly<UnionU, D>, _ExcludeExactly<DeleteT, D>>
: never;
type _ExcludeExactly<UnionU, DeleteT> =
true extends IsAny<DeleteT>
? never
: true extends IsUnknown<DeleteT>
? never
: UnionU extends unknown // Only for union distribution.
? MatchOrNever<UnionU, DeleteT>
: never;

export {};
34 changes: 34 additions & 0 deletions source/internal/type.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ 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 {SimplifyDeep} from '../simplify-deep.d.ts';
import type {UnionToTuple} from '../union-to-tuple.d.ts';

/**
Matches any primitive, `void`, `Date`, or `RegExp` value.
Expand Down Expand Up @@ -161,4 +163,36 @@ 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 = UniqueUnionDeep<{z: {a: {aa: 0} | {aa: 0}} | {a: {aa: 0} | {aa: 0}} | {b: 0}; x: '1'}>; // => {z: {a: {aa: 0}} | {b: 0}; x: '1'}
type UniqueUnionDeepKeepDistributionTest = UniqueUnionDeep<{z: {a: 0} | {a: 0}; x: '1'} | {z: {a: 0} | {a: 0}; x: '2'}>; // => {z: {a: 0}; x: '1'} | {z: {a: 0}; x: '2'}
```

To remove delayed intersection, use `SimplifyDeep`.
And use `SimplifyDeep<UniqueUnionDeep<A>>` if eliminating duplicated union and intersection.
*/
export type UniqueUnionDeep<U> = U extends object ? SimplifyDeep<_UniqueUnionDeep<U>> : U;
type _UniqueUnionDeep<U extends object> = {[K in keyof U]: U[K] extends object ? UniqueUnion<_UniqueUnionDeep<U[K]>> : U[K]};

/**
The flat version of `UniqueUnionDeep`.
*/
export type UniqueUnion<U> =
UnionToTuple<U> extends infer E extends readonly unknown[] // Improve performance.
? E[number]
: never; // Unreachable.

export {};
26 changes: 20 additions & 6 deletions source/is-equal.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {IsNever} from './is-never.d.ts';
import type {UniqueUnionDeep} from './internal/type.d.ts';

/**
Returns a boolean for whether the two given types are equal.

Expand Down Expand Up @@ -26,13 +27,26 @@ type Includes<Value extends readonly any[], Item> =
@category Utilities
*/
export type IsEqual<A, B> =
[A] extends [B]
? [B] extends [A]
? _IsEqual<A, B>
: false
[A, B] extends [B, A]
? [A, B] extends [object, object]
? _IsEqual<UniqueUnionDeep<A>, UniqueUnionDeep<B>>
: _IsEqual<A, B>
: 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<A, B> =
(<G>() => G extends A & G | G ? 1 : 2) extends
(<G>() => G extends B & G | G ? 1 : 2)
Expand Down
41 changes: 41 additions & 0 deletions source/last-of-union.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type {IsNever} from './is-never.d.ts';
import type {UnionToIntersection} from './union-to-intersection.d.ts';

/**
Returns the last element of a union type; otherwise `never` if `never` passed.
Note that this is non-deterministic because the order of union type is not guaranteed.

@see https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468375328

This can be used to implement a recursive type function that accepts a union type.
It can detect a termination case using {@link IsNever `IsNever`}.

@example
```
import type {LastOfUnion, ExcludeExactly, IsNever} from 'type-fest';

export type UnionToTuple<T, L = LastOfUnion<T>> =
IsNever<T> extends false
? [...UnionToTuple<ExcludeExactly<T, L>>, L]
: [];
```

@example
```
import type {LastOfUnion} from 'type-fest';

type Last = LastOfUnion<1 | 2 | 3>;
//=> 3

type LastNever = LastOfUnion<never>;
//=> never
```
*/
export type LastOfUnion<T> =
true extends IsNever<T>
? never
: UnionToIntersection<T extends any ? () => T : never> extends () => (infer R)
? R
: never;

export {};
21 changes: 5 additions & 16 deletions source/union-to-tuple.d.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,6 @@
import type {IsNever} from './is-never.d.ts';
import type {UnionToIntersection} from './union-to-intersection.d.ts';

/**
Returns the last element of a union type.

@example
```
type Last = LastOfUnion<1 | 2 | 3>;
//=> 3
```
*/
type LastOfUnion<T> =
UnionToIntersection<T extends any ? () => T : never> extends () => (infer R)
? R
: never;
import type {ExcludeExactly} from './exclude-exactly.d.ts';
import type {LastOfUnion} from './last-of-union.d.ts';

/**
Convert a union type into an unordered tuple type of its elements.
Expand Down Expand Up @@ -52,7 +39,9 @@ const petList = Object.keys(pets) as UnionToTuple<Pet>;
*/
export type UnionToTuple<T, L = LastOfUnion<T>> =
IsNever<T> extends false
? [...UnionToTuple<Exclude<T, L>>, L]
? ExcludeExactly<T, L> extends infer E // Improve performance.
? [...UnionToTuple<E>, L]
: never // Unreachable.
: [];

export {};
39 changes: 39 additions & 0 deletions test-d/exclude-exactly.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {expectType} from 'tsd';
import type {ExcludeExactly} from '../index.d.ts';

expectType<number>({} as ExcludeExactly<0 | 1 | number, '1'>);
expectType<never>({} as ExcludeExactly<0 | 1 | number, number>);
expectType<string>({} as ExcludeExactly<'0' | '1' | string, '1'>);
expectType<never>({} as ExcludeExactly<'0' | '1' | string, string>);

// `{readonly a: t}` should not be equal to `{a: t}` because of assignability.
expectType<{a: 0}>({} as ExcludeExactly<{a: 0} | {readonly a: 0}, {readonly a: 0}>);
expectType<{readonly a: 0}>({} as ExcludeExactly<{readonly a: 0}, {a: 0}>);
expectType<never>({} as ExcludeExactly<{readonly a: 0}, {readonly a: 0}>);

// `never` does nothing.
expectType<0 | 1 | 2>({} as ExcludeExactly<0 | 1 | 2, never>);
expectType<never>({} as ExcludeExactly<never, never>);

// `unknown` cannot be excluded like `unknown\T` in any cases.
expectType<unknown>({} as ExcludeExactly<unknown, string>);
expectType<[unknown]>({} as ExcludeExactly<[unknown], [number]>);
expectType<unknown[]>({} as ExcludeExactly<unknown[], number[]>);
expectType<{a: unknown}>({} as ExcludeExactly<{a: unknown}, {a: number}>);
expectType<unknown[]>({} as ExcludeExactly<number[] | unknown[], number[]>);

// `unknown` and `any` exclude themselves.
expectType<never>({} as ExcludeExactly<unknown, unknown>);
expectType<never>({} as ExcludeExactly<unknown, any>);
expectType<never>({} as ExcludeExactly<any, any>);
expectType<never>({} as ExcludeExactly<any, unknown>);

// `unknown` and `any` exclude other types.
expectType<never>({} as ExcludeExactly<string | number, unknown>);
expectType<never>({} as ExcludeExactly<string | number, any>);

// Union
expectType<2>({} as ExcludeExactly<0 | 1 | 2, 0 | 1>);
expectType<never>({} as ExcludeExactly<0 | 1 | 2, 0 | 1 | 2>);
expectType<{readonly a?: 0}>({} as ExcludeExactly<{a: 0} | {readonly a: 0} | {a?: 0} | {readonly a?: 0}, {a: 0} | {readonly a: 0} | {a?: 0}>);
expectType<never>({} as ExcludeExactly<{a: 0} | {readonly a: 0} | {a?: 0} | {readonly a?: 0}, {a: 0} | {readonly a: 0} | {a?: 0} | {readonly a?: 0}>);
33 changes: 33 additions & 0 deletions test-d/internal/unique-union-deep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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<never>({} as UniqueUnionDeep<never>);
expectType<string>({} as UniqueUnionDeep<string>);
expectType<{a: 0}>({} as UniqueUnionDeep<{a: 0}>);
expectType<unknown>({} as UniqueUnionDeep<unknown>);
expectType<any>({} as UniqueUnionDeep<any>);
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
27 changes: 27 additions & 0 deletions test-d/internal/unique-union.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {expectType} from 'tsd';
import type {UniqueUnion} from '../../source/internal/type.d.ts';

expectType<never>({} as UniqueUnion<never>);
expectType<string>({} as UniqueUnion<string>);
expectType<{a: 0}>({} as UniqueUnion<{a: 0}>);
expectType<unknown>({} as UniqueUnion<unknown>);
expectType<any>({} as UniqueUnion<any>);
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
Loading