Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ccb31dd
add: `LastOfUnion`, return a type of an union-type (order is not guar…
taiyakihitotsu Feb 5, 2026
397ebf4
refactor: `UnionToTuple`, import `LastOfUnion` instead of define
taiyakihitotsu Feb 5, 2026
167f3eb
Add `ExcludeExactly`, distinguish between different modifiers.
taiyakihitotsu Feb 5, 2026
08bc31c
fix: `UnionToTuple`, use `ExcludeExactly`, improve performance.
taiyakihitotsu Feb 5, 2026
b24f977
chore: fix doc
taiyakihitotsu Feb 5, 2026
0dbb9c0
doc: `ExcludeExactly`, fix arrow test cases
taiyakihitotsu Feb 5, 2026
7ef927a
Update last-of-union.ts
sindresorhus Feb 6, 2026
e95c1ee
doc: address review
taiyakihitotsu Feb 6, 2026
3b54932
test: `UnionToTuple`, super type test cases
taiyakihitotsu Feb 6, 2026
8b984c9
test: add test cases
taiyakihitotsu Feb 6, 2026
4baac7e
update: refine `ExcludeExactly`, update test-cases.
taiyakihitotsu Feb 7, 2026
0699dc0
update: move `SimpleIsEqual` to `internal`
taiyakihitotsu Feb 7, 2026
01cbb49
test: `LastOfUnion` ensures all union members would be picked.
taiyakihitotsu Feb 7, 2026
408b2de
test: `test-d/last-of-union.ts`, remove `any` and `unknown` checker t…
taiyakihitotsu Feb 7, 2026
614ad2a
test: add edge cases.
taiyakihitotsu Feb 10, 2026
e4bd1e8
update: remove `SimpleIsEqual`, move `LastOfUnion` to `internal`, del…
taiyakihitotsu Feb 11, 2026
65cc0bd
refactor: remove unused imports and tests
taiyakihitotsu Feb 12, 2026
68c4798
doc: cleanup JSDoc
som-sm Feb 13, 2026
78fcde9
fix: README description
som-sm Feb 13, 2026
6822834
test: improve `ExcludeExactly` tests
som-sm Feb 13, 2026
5dce64e
test: add note regarding `DifferentModifierUnion`
som-sm Feb 13, 2026
9579825
refactor: revert `LastOfUnion` and `IsEqual` comment, delete unused t…
taiyakihitotsu Feb 13, 2026
2d728ee
chore: remove `LastOfUnion` changes
som-sm Feb 15, 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 member of a 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
69 changes: 69 additions & 0 deletions source/exclude-exactly.d.ts
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this implementation simpler/better? It passes all the existing tests and also works as suggested with any and unknown.

export type ExcludeExactly<Union, Delete> =
	IfNotAnyOrNever<
		Union,
		_ExcludeExactly<Union, Delete>,
		// If `Union` is `any`, then if `Delete` is `any`, return `never`, else return `Union`.
		If<IsAny<Delete>, never, Union>,
		// If `Union` is `never`, then if `Delete` is `never`, return `never`, else return `Union`.
		If<IsNever<Delete>, never, Union>
	>;

type _ExcludeExactly<Union, Delete> =
	IfNotAnyOrNever<Delete,
		Union extends unknown // For distributing `Union`
			? [Delete extends unknown // For distributing `Delete`
				? If<SimpleIsEqual<Union, Delete>, 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
	>;

type SimpleIsEqual<A, B> =
	(<G>() => G extends A & G | G ? 1 : 2) extends
	(<G>() => G extends B & G | G ? 1 : 2)
		? true
		: false;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your definition doesn't use LastOfUnion and is clearer about the comparison part, since the SimpleIsEqual pattern is well-known, and all tests pass.
But SimpleIsEqual is already defined in is-equal.d.ts, so should we move it to internal, or even export it? Is that ok?
(I vaguely think it is acceptable because this comparison construct is useful to define some type functions.)

And in this situation, LastOfUnion is still worth exporting as a standalone utility.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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 {IfNotAnyOrNever, SimpleIsEqual} from './internal/type.d.ts';

/**
A stricter version of `Exclude<T, U>` that ensures objects with different key modifiers are not considered identical.

TypeScript's built-in `Exclude` and `ExcludeStrict` in `type-fest` don't distinguish key modifiers 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
```

`ExcludeExactly` keeps the union members element if the members are not 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}
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}>;
//=> never
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[]
```

@category Improved Built-in
*/
export type ExcludeExactly<Union, Delete> =
IfNotAnyOrNever<
Union,
_ExcludeExactly<Union, Delete>,
// If `Union` is `any`, then if `Delete` is `any`, return `never`, else return `Union`.
If<IsAny<Delete>, never, Union>,
// If `Union` is `never`, then if `Delete` is `never`, return `never`, else return `Union`.
If<IsNever<Delete>, never, Union>
>;

type _ExcludeExactly<Union, Delete> =
IfNotAnyOrNever<Delete,
Union extends unknown // For distributing `Union`
? [Delete extends unknown // For distributing `Delete`
? If<SimpleIsEqual<Union, Delete>, 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 {};
24 changes: 24 additions & 0 deletions source/internal/type.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,28 @@ export type IsExactOptionalPropertyTypesEnabled = [(string | undefined)?] extend
? false
: true;

/**
A simple version of `IsEqual`.

`SimpleIsEqual<never, unknown>` and `SimpleIsEqual<unknown, never>` return `true`, whereas `IsEqual` returns `false` correctly.

`SimpleIsEqual` doesn't return `false` correctly for identical union/intersection arguments.

@example
```
type UnionCase = SimpleIsEqual<{a: {b: 0} | {b: 0}}, {a: {b: 0}}>;
//=> false

type IntersectionCase = SimpleIsEqual<{a: {b: 0} & {b: 0}}, {a: {b: 0}}>;
//=> false
```

`SimpleIsEqual` fails the `equalWrappedTupleIntersectionToBeNeverAndNeverExpanded` test in `test-d/internal/simple-is-equal.ts`.
*/
export type SimpleIsEqual<A, B> =
(<G>() => G extends A & G | G ? 1 : 2) extends
(<G>() => G extends B & G | G ? 1 : 2)
? true
: false;

export {};
12 changes: 3 additions & 9 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 {SimpleIsEqual} from './internal/type.d.ts';

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

Expand Down Expand Up @@ -28,15 +29,8 @@ type Includes<Value extends readonly any[], Item> =
export type IsEqual<A, B> =
[A] extends [B]
? [B] extends [A]
? _IsEqual<A, B>
? SimpleIsEqual<A, B>
: false
: 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)
? true
: false;

export {};
46 changes: 46 additions & 0 deletions source/last-of-union.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type {IsNever} from './is-never.d.ts';
import type {UnionToIntersection} from './union-to-intersection.d.ts';

/**
Return a member of a union type. Order is not guaranteed.
Returns `never` when the input is `never`.

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

Use-cases:
- Implementing recursive type functions that accept a union type.
- Reducing a union one member at a time, for example when building tuples.

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
```

@category Type
*/
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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please create another PR for the purpose of exposing LastOfUnion and move all these changes there.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep this PR only for fixing UnionToTuple and adding new ExcludeExactly type.

Copy link
Copy Markdown
Contributor Author

@taiyakihitotsu taiyakihitotsu Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@som-sm

Yes.
I've opened LastOfUnion PR, and updated this PR's title.

Note:
test-d/last-of-union.ts in #1368 uses the old (my defined) ExcludeExactly which is written with LastOfUnion, because LastOfUnion should also be guaranteed to keep its union members.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the expected workflow:

How does this look?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR uses the old (my defined) ExcludeExactly which is written with LastOfUnion, because LastOfUnion should also be guaranteed to keep its union members.

Didn't get this. Can't see ExcludeExactly using LastOfUnion.

Copy link
Copy Markdown
Contributor Author

@taiyakihitotsu taiyakihitotsu Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can there be a test that validates this perf improvement?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can there be a test that validates this perf improvement?

Currently, no.
I haven’t started working on this yet...


(This is the comment about this gc issue I posted firstly.)

I made a branch to reproduce this.

This commit, taiyakihitotsu@e8f0967, fixes the issue.
And npm test will crash if this is reverted.


The error logs don't explicitly point to UnionToTuple as the cause, and it’s not a standard TS2589 error.

We might need to implement granular tests for individual type definitions within the package, rather than just running a full type-check on test files.
Without these, it’s difficult to pinpoint exactly where the performance bottleneck or crash is originating.

Also, if we do implement such tests, we’ll need to separately discuss where to set the thresholds (e.g., memory limits or complexity scores).

? [...UnionToTuple<E>, L]
: never // Unreachable.
: [];

export {};
49 changes: 49 additions & 0 deletions test-d/exclude-exactly.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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<0>({} as ExcludeExactly<0, 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>);

// Edge cases.
expectType<never>({} as ExcludeExactly<never, never>);
expectType<any>({} as ExcludeExactly<any, never>);
expectType<unknown>({} as ExcludeExactly<unknown, 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` excludes `unknown`, `any` excludes `any`.
expectType<never>({} as ExcludeExactly<unknown, unknown>);
expectType<unknown>({} as ExcludeExactly<unknown, any>);
expectType<never>({} as ExcludeExactly<any, any>);
expectType<any>({} as ExcludeExactly<any, unknown>);
expectType<string | number>({} as ExcludeExactly<string | number, unknown>);
expectType<string | number>({} 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}>);

// Identical Union
expectType<never>({} as ExcludeExactly<{a: 0} | {a: 0}, {a: 0}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents

// Identical Intersection
expectType<never>({} as ExcludeExactly<{a: 0} & {a: 0}, {a: 0}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents
95 changes: 95 additions & 0 deletions test-d/internal/simple-is-equal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {expectType} from 'tsd';
import type {TupleOf} from '../../index.d.ts';
import type {SimpleIsEqual} from '../../source/internal/index.d.ts';

expectType<false>({} as SimpleIsEqual<number, string>);
expectType<true>({} as SimpleIsEqual<1, 1>);
expectType<false>({} as SimpleIsEqual<'A', 'B'>);
expectType<true>({} as SimpleIsEqual<'foo', 'foo'>);
expectType<false>({} as SimpleIsEqual<true, false>);
expectType<true>({} as SimpleIsEqual<false, false>);

expectType<false>({} as SimpleIsEqual<any, number>);
expectType<false>({} as SimpleIsEqual<'', never>);
expectType<true>({} as SimpleIsEqual<any, any>);
expectType<true>({} as SimpleIsEqual<never, never>);
expectType<false>({} as SimpleIsEqual<any, never>);
expectType<false>({} as SimpleIsEqual<never, any>);
expectType<false>({} as SimpleIsEqual<any, unknown>);
// `IsEqual` returns `false`, `SimpleIsEqual` returns `true`.
expectType<true>({} as SimpleIsEqual<never, unknown>);
// `IsEqual` returns `false`, `SimpleIsEqual` returns `true`.
expectType<true>({} as SimpleIsEqual<unknown, never>);
expectType<false>({} as SimpleIsEqual<[never], [unknown]>);
expectType<false>({} as SimpleIsEqual<[unknown], [never]>);
expectType<false>({} as SimpleIsEqual<[any], [never]>);
expectType<true>({} as SimpleIsEqual<[any], [any]>);
expectType<true>({} as SimpleIsEqual<[never], [never]>);

expectType<false>({} as SimpleIsEqual<1 | 2, 1>);
expectType<false>({} as SimpleIsEqual<1 | 2, 2 | 3>);
expectType<true>({} as SimpleIsEqual<1 | 2, 2 | 1>);
expectType<false>({} as SimpleIsEqual<boolean, true>);

expectType<true>({} as SimpleIsEqual<{a: 1}, {a: 1}>);
expectType<false>({} as SimpleIsEqual<{a: 1}, {a?: 1}>);
expectType<false>({} as SimpleIsEqual<{a: 1}, {readonly a: 1}>);

expectType<true>({} as SimpleIsEqual<[], []>);
expectType<true>({} as SimpleIsEqual<readonly [], readonly []>);
expectType<false>({} as SimpleIsEqual<readonly [], []>);
expectType<true>({} as SimpleIsEqual<number[], number[]>);
expectType<true>({} as SimpleIsEqual<readonly number[], readonly number[]>);
expectType<false>({} as SimpleIsEqual<readonly number[], number[]>);
expectType<true>({} as SimpleIsEqual<[string], [string]>);
expectType<false>({} as SimpleIsEqual<[string], [string, number]>);
expectType<false>({} as SimpleIsEqual<[0, 1] | [0, 2], [0, 2]>);

type LongTupleNumber = TupleOf<50, 0>;
expectType<true>({} as SimpleIsEqual<LongTupleNumber, LongTupleNumber>);

type ReadonlyLongTupleNumber = Readonly<TupleOf<50, 0>>;
expectType<true>({} as SimpleIsEqual<ReadonlyLongTupleNumber, ReadonlyLongTupleNumber>);

expectType<false>({} as SimpleIsEqual<ReadonlyLongTupleNumber, LongTupleNumber>);

// Missing all generic parameters.
// @ts-expect-error
type A = SimpleIsEqual;

// Missing `Y` generic parameter.
// @ts-expect-error
type B = SimpleIsEqual<number>;

// Test for issue https://github.com/sindresorhus/type-fest/issues/537
type UnionType = SimpleIsEqual<{a: 1} | {a: 1}, {a: 1}>; // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents
expectType<UnionType>(true);

type IntersectionType = SimpleIsEqual<{a: 1} & {a: 1}, {a: 1}>; // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents
expectType<IntersectionType>(true);

// Test for PR https://github.com/sindresorhus/type-fest/pull/1231
type BranchOnWrappedTupleMatches<Tpl> = (Tpl extends [[0, 2]] ? 'Foo' : 'Bar');
type BranchOnWrappedTupleDoesNotMatch<Tpl> = (Tpl extends [[0, 1]] ? 'Foo' : 'Bar');
type BranchOnTupleMatches<Tpl> = (Tpl extends [0, 2] ? 'Foo' : 'Bar');
type BranchOnTupleDoesNotMatch<Tpl> = (Tpl extends [0, 1] ? 'Foo' : 'Bar');

declare const equalWrappedTupleIntersectionToBeNeverAndNever: SimpleIsEqual<(BranchOnWrappedTupleMatches<[[0, 2]]> & BranchOnWrappedTupleDoesNotMatch<[[0, 2]]>), never>;
expectType<true>(equalWrappedTupleIntersectionToBeNeverAndNever);

// `IsEqual` returns `false`, `SimpleIsEqual` returns `true`.
declare const equalWrappedTupleIntersectionToBeNeverAndNeverExpanded: [0, 2] extends infer Tpl ? SimpleIsEqual<(BranchOnWrappedTupleMatches<[Tpl]> & BranchOnWrappedTupleDoesNotMatch<[Tpl]>), never> : never;
expectType<false>(equalWrappedTupleIntersectionToBeNeverAndNeverExpanded);

declare const equalTupleIntersectionToBeNeverAndNever: SimpleIsEqual<(BranchOnTupleMatches<[0, 2]> & BranchOnTupleDoesNotMatch<[0, 2]>), never>;
expectType<true>(equalTupleIntersectionToBeNeverAndNever);

declare const equalTupleIntersectionToBeNeverAndNeverExpanded: [0, 2] extends infer Tpl ? SimpleIsEqual<(BranchOnTupleMatches<Tpl> & BranchOnTupleDoesNotMatch<Tpl>), never> : never;
expectType<true>(equalTupleIntersectionToBeNeverAndNeverExpanded);

declare const equalTupleIntersectionAndTuple: SimpleIsEqual<[{a: 1}] & [{a: 1}], [{a: 1}]>; // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents
expectType<true>(equalTupleIntersectionAndTuple);

// Test for Issue https://github.com/sindresorhus/type-fest/issues/1305
type Assignability<T, U, _V extends SimpleIsEqual<T, U>> = any;
type TestAssignability<T> = Assignability<T, T, true>;
Loading