From ccb31dd7a302852dc79becf60385ef4cd1618c91 Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Thu, 5 Feb 2026 23:37:09 +0900 Subject: [PATCH 01/23] add: `LastOfUnion`, return a type of an union-type (order is not guaranteed) --- index.d.ts | 1 + readme.md | 1 + source/last-of-union.d.ts | 41 +++++++++++++++++++++++++++++++++++++++ test-d/last-of-union.ts | 10 ++++++++++ 4 files changed, 53 insertions(+) create mode 100644 source/last-of-union.d.ts create mode 100644 test-d/last-of-union.ts diff --git a/index.d.ts b/index.d.ts index 8a9926d8d..f859bce03 100644 --- a/index.d.ts +++ b/index.d.ts @@ -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'; diff --git a/readme.md b/readme.md index 47e9fee30..55d202543 100644 --- a/readme.md +++ b/readme.md @@ -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 diff --git a/source/last-of-union.d.ts b/source/last-of-union.d.ts new file mode 100644 index 000000000..7d2dfa8d7 --- /dev/null +++ b/source/last-of-union.d.ts @@ -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> = + IsNever extends false + ? [...UnionToTuple>, L] + : []; +``` + +@example +``` +import type {LastOfUnion} from 'type-fest'; + +type Last = LastOfUnion<1 | 2 | 3>; +//=> 3 + +type LastNever = LastOfUnion; +//=> never +``` +*/ +export type LastOfUnion = + true extends IsNever + ? never + : UnionToIntersection T : never> extends () => (infer R) + ? R + : never; + +export {}; diff --git a/test-d/last-of-union.ts b/test-d/last-of-union.ts new file mode 100644 index 000000000..c3b904267 --- /dev/null +++ b/test-d/last-of-union.ts @@ -0,0 +1,10 @@ +import {expectType} from 'tsd'; +import type {LastOfUnion} from '../index.d.ts'; + +// `LastOfUnion` distinguish between different modifiers. +type UnionType = {a: 0} | {b: 0} | {a?: 0} | {readonly a?: 0} | {readonly a: 0}; +expectType({} as LastOfUnion extends UnionType ? true : false); +expectType({} as UnionType extends LastOfUnion ? true : false); + +// `never` act as a termination condition with `IsNever`. +expectType({} as LastOfUnion); From 397ebf4ce8d54ffa4af4cab9ff3160590dba4aa7 Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Thu, 5 Feb 2026 23:38:25 +0900 Subject: [PATCH 02/23] refactor: `UnionToTuple`, import `LastOfUnion` instead of define --- source/union-to-tuple.d.ts | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/source/union-to-tuple.d.ts b/source/union-to-tuple.d.ts index 5a6d13ae2..98ab73cf3 100644 --- a/source/union-to-tuple.d.ts +++ b/source/union-to-tuple.d.ts @@ -1,19 +1,5 @@ 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 = -UnionToIntersection T : never> extends () => (infer R) - ? R - : never; +import type {LastOfUnion} from './last-of-union.d.ts'; /** Convert a union type into an unordered tuple type of its elements. From 167f3eb792ea1badc9f3c7894ce09faf48da5e53 Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Thu, 5 Feb 2026 23:43:30 +0900 Subject: [PATCH 03/23] Add `ExcludeExactly`, distinguish between different modifiers. --- index.d.ts | 1 + readme.md | 1 + source/exclude-exactly.d.ts | 98 +++++++++++++++++++++++++++++++++++++ test-d/exclude-exactly.ts | 39 +++++++++++++++ 4 files changed, 139 insertions(+) create mode 100644 source/exclude-exactly.d.ts create mode 100644 test-d/exclude-exactly.ts diff --git a/index.d.ts b/index.d.ts index f859bce03..42afe12b5 100644 --- a/index.d.ts +++ b/index.d.ts @@ -208,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 {}; diff --git a/readme.md b/readme.md index 55d202543..6cc0b5b87 100644 --- a/readme.md +++ b/readme.md @@ -320,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` that ensures every member of `U` can successfully extract something from `T`. - [`ExcludeStrict`](source/exclude-strict.d.ts) - A stricter version of `Exclude` that ensures every member of `U` can successfully exclude something from `T`. +- [`ExcludeExactly`](source/exclude-exactly.d.ts) - A stricter version of `Exclude` that ensures objects with different key modifiers are not considered identical. ## Declined types diff --git a/source/exclude-exactly.d.ts b/source/exclude-exactly.d.ts new file mode 100644 index 000000000..0fa751307 --- /dev/null +++ b/source/exclude-exactly.d.ts @@ -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 +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} +``` + +`unknown` and `never` cases, which easily break equality in type level code base. + +@example +``` +type E = MatchOrNever; // => unknown +type F = MatchOrNever; // => never +type G = MatchOrNever; // => never +type H = MatchOrNever; // => 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 +``` +*/ +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; + +/** +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} +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} +type ExcludeSubType = ExcludeExactly<0 | 1 | number, 1>; // => number +type ExcludeAllSet = ExcludeExactly<0 | 1 | number, number>; // => never +type ExcludeFromUnknown = ExcludeExactly; // => unknown +type ExcludeFromUnknownArray = ExcludeExactly; // => unknown[] +``` +*/ +export type ExcludeExactly = + LastOfUnion extends infer D + ? true extends IsNever + ? UnionU + : ExcludeExactly<_ExcludeExactly, _ExcludeExactly> + : never; +type _ExcludeExactly = + true extends IsAny + ? never + : true extends IsUnknown + ? never + : UnionU extends unknown // Only for union distribution. + ? MatchOrNever + : never; + +export {}; diff --git a/test-d/exclude-exactly.ts b/test-d/exclude-exactly.ts new file mode 100644 index 000000000..935a30c3b --- /dev/null +++ b/test-d/exclude-exactly.ts @@ -0,0 +1,39 @@ +import {expectType} from 'tsd'; +import type {ExcludeExactly} from '../index.d.ts'; + +expectType({} as ExcludeExactly<0 | 1 | number, '1'>); +expectType({} as ExcludeExactly<0 | 1 | number, number>); +expectType({} as ExcludeExactly<'0' | '1' | string, '1'>); +expectType({} 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({} as ExcludeExactly<{readonly a: 0}, {readonly a: 0}>); + +// `never` does nothing. +expectType<0 | 1 | 2>({} as ExcludeExactly<0 | 1 | 2, never>); +expectType({} as ExcludeExactly); + +// `unknown` cannot be excluded like `unknown\T` in any cases. +expectType({} as ExcludeExactly); +expectType<[unknown]>({} as ExcludeExactly<[unknown], [number]>); +expectType({} as ExcludeExactly); +expectType<{a: unknown}>({} as ExcludeExactly<{a: unknown}, {a: number}>); +expectType({} as ExcludeExactly); + +// `unknown` and `any` exclude themselves. +expectType({} as ExcludeExactly); +expectType({} as ExcludeExactly); +expectType({} as ExcludeExactly); +expectType({} as ExcludeExactly); + +// `unknown` and `any` exclude other types. +expectType({} as ExcludeExactly); +expectType({} as ExcludeExactly); + +// Union +expectType<2>({} as ExcludeExactly<0 | 1 | 2, 0 | 1>); +expectType({} 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({} as ExcludeExactly<{a: 0} | {readonly a: 0} | {a?: 0} | {readonly a?: 0}, {a: 0} | {readonly a: 0} | {a?: 0} | {readonly a?: 0}>); From 08bc31cf2994c77132abb28c489bb3cfc07234bd Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Thu, 5 Feb 2026 23:44:51 +0900 Subject: [PATCH 04/23] fix: `UnionToTuple`, use `ExcludeExactly`, improve performance. --- source/union-to-tuple.d.ts | 5 ++++- test-d/union-to-tuple.ts | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/source/union-to-tuple.d.ts b/source/union-to-tuple.d.ts index 98ab73cf3..f9184f7d9 100644 --- a/source/union-to-tuple.d.ts +++ b/source/union-to-tuple.d.ts @@ -1,4 +1,5 @@ import type {IsNever} from './is-never.d.ts'; +import type {ExcludeExactly} from './exclude-exactly.d.ts'; import type {LastOfUnion} from './last-of-union.d.ts'; /** @@ -38,7 +39,9 @@ const petList = Object.keys(pets) as UnionToTuple; */ export type UnionToTuple> = IsNever extends false - ? [...UnionToTuple>, L] + ? ExcludeExactly extends infer E // Improve performance. + ? [...UnionToTuple, L] + : never // Unreachable. : []; export {}; diff --git a/test-d/union-to-tuple.ts b/test-d/union-to-tuple.ts index 12de3cf2b..1dcaec026 100644 --- a/test-d/union-to-tuple.ts +++ b/test-d/union-to-tuple.ts @@ -11,3 +11,6 @@ expectType({} as (1 | 2 | 3)); type Options2 = UnionToTuple; expectType({} as (1 | false | true)); + +type DifferentModifiers = {a: 0} | {readonly a: 0} | {a?: 0} | {readonly a?: 0}; +expectType[number]>({} as DifferentModifiers); From b24f977d89e198548087be8654ff7602399a0e64 Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Fri, 6 Feb 2026 01:49:08 +0900 Subject: [PATCH 05/23] chore: fix doc --- readme.md | 2 +- source/exclude-exactly.d.ts | 4 ++-- source/last-of-union.d.ts | 4 ++-- test-d/last-of-union.ts | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/readme.md b/readme.md index 6cc0b5b87..55ca665c6 100644 --- a/readme.md +++ b/readme.md @@ -191,7 +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. +- [`LastOfUnion`](source/last-of-union.d.ts) - Return a type picked in a union type. Order is not guaranteed. ### Type Guard diff --git a/source/exclude-exactly.d.ts b/source/exclude-exactly.d.ts index 0fa751307..f112b69db 100644 --- a/source/exclude-exactly.d.ts +++ b/source/exclude-exactly.d.ts @@ -54,7 +54,7 @@ type MatchOrNever = : A; /** -TypeScript's built-in `Exclude` and `ExcludeStrict` in `type-fest` don't distinguish kinds of keys of objects. +TypeScript's built-in `Exclude` and `ExcludeStrict` in `type-fest` don't distinguishes key modifiers of objects. @example ``` @@ -64,7 +64,7 @@ type NeverReturned_0 = Exclude<{a: 0} | {readonly a: 0}, {readonly a: 0}>; // => 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. +`ExcludeExactly` keeps the union members element if the members are not identical. @example ``` diff --git a/source/last-of-union.d.ts b/source/last-of-union.d.ts index 7d2dfa8d7..0c14870a9 100644 --- a/source/last-of-union.d.ts +++ b/source/last-of-union.d.ts @@ -2,8 +2,8 @@ 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. +Returns the last element of a union type; otherwise returns `never` if `never` is passed. +Note that this is non-deterministic because the order of union members is not guaranteed. @see https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468375328 diff --git a/test-d/last-of-union.ts b/test-d/last-of-union.ts index c3b904267..04bbf306d 100644 --- a/test-d/last-of-union.ts +++ b/test-d/last-of-union.ts @@ -1,10 +1,10 @@ import {expectType} from 'tsd'; import type {LastOfUnion} from '../index.d.ts'; -// `LastOfUnion` distinguish between different modifiers. +// `LastOfUnion` distinguishes between different modifiers. type UnionType = {a: 0} | {b: 0} | {a?: 0} | {readonly a?: 0} | {readonly a: 0}; expectType({} as LastOfUnion extends UnionType ? true : false); expectType({} as UnionType extends LastOfUnion ? true : false); -// `never` act as a termination condition with `IsNever`. +// `never` acts as a termination condition with `IsNever`. expectType({} as LastOfUnion); From 0dbb9c0a73a5ea613925f62401bd81148f2e44a9 Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Fri, 6 Feb 2026 06:19:29 +0900 Subject: [PATCH 06/23] doc: `ExcludeExactly`, fix arrow test cases --- source/exclude-exactly.d.ts | 68 ++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 23 deletions(-) diff --git a/source/exclude-exactly.d.ts b/source/exclude-exactly.d.ts index f112b69db..4515b56a0 100644 --- a/source/exclude-exactly.d.ts +++ b/source/exclude-exactly.d.ts @@ -10,28 +10,38 @@ Return the 1st if not. @example ``` -type A = MatchOrNever; // => string | number -type B = MatchOrNever; // => never -type C = MatchOrNever; // => string | number -type D = MatchOrNever; // => string +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} +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 -type F = MatchOrNever; // => never -type G = MatchOrNever; // => never -type H = MatchOrNever; // => never +type E = MatchOrNever; +//=> unknown +type F = MatchOrNever; +//=> never +type G = MatchOrNever; +//=> never +type H = MatchOrNever; +//=> never ``` Note that this doesn't regard the identical union/intersection type `T | T` and/or `T & T` as `T` recursively. @@ -39,9 +49,11 @@ 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 IDUnion = MatchOrNever<{a: {b: 0}} | {a: {b: 0}}, {a: {b: 0}}>; +//=> never type A = {a: {b: 0} | {b: 0}}; -type RecurivelyIDUnion = MatchOrNever; // => A +type RecurivelyIDUnion = MatchOrNever; +//=> A ``` */ type MatchOrNever = @@ -60,8 +72,10 @@ TypeScript's built-in `Exclude` and `ExcludeStrict` in `type-fest` don't disting ``` 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 +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. @@ -70,14 +84,22 @@ type NeverReturned_1 = ExcludeStrict<{a: 0} | {readonly a: 0}, {readonly a: 0}>; ``` 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}>; // => {readonly a: 0} -type ExcludeSubType = ExcludeExactly<0 | 1 | number, 1>; // => number -type ExcludeAllSet = ExcludeExactly<0 | 1 | number, number>; // => never -type ExcludeFromUnknown = ExcludeExactly; // => unknown -type ExcludeFromUnknownArray = ExcludeExactly; // => unknown[] +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 +type ExcludeFromUnknownArray = ExcludeExactly; +//=> unknown[] ``` */ export type ExcludeExactly = @@ -86,6 +108,7 @@ export type ExcludeExactly = ? UnionU : ExcludeExactly<_ExcludeExactly, _ExcludeExactly> : never; + type _ExcludeExactly = true extends IsAny ? never @@ -94,5 +117,4 @@ type _ExcludeExactly = : UnionU extends unknown // Only for union distribution. ? MatchOrNever : never; - export {}; From 7ef927a2b872426c2b2105c67c57f8919af1c4ab Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Fri, 6 Feb 2026 14:26:51 +0700 Subject: [PATCH 07/23] Update last-of-union.ts --- test-d/last-of-union.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test-d/last-of-union.ts b/test-d/last-of-union.ts index 04bbf306d..f774fb9bd 100644 --- a/test-d/last-of-union.ts +++ b/test-d/last-of-union.ts @@ -1,5 +1,5 @@ import {expectType} from 'tsd'; -import type {LastOfUnion} from '../index.d.ts'; +import type {LastOfUnion, IsAny, IsUnknown} from '../index.d.ts'; // `LastOfUnion` distinguishes between different modifiers. type UnionType = {a: 0} | {b: 0} | {a?: 0} | {readonly a?: 0} | {readonly a: 0}; @@ -8,3 +8,6 @@ expectType({} as UnionType extends LastOfUnion ? true : false) // `never` acts as a termination condition with `IsNever`. expectType({} as LastOfUnion); + +expectType({} as IsUnknown>); +expectType({} as IsAny>); From e95c1eec91ba947d1ed44ce2261e7cfc180f1029 Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Fri, 6 Feb 2026 17:33:29 +0900 Subject: [PATCH 08/23] doc: address review --- readme.md | 2 +- source/exclude-exactly.d.ts | 14 +++++++++----- source/last-of-union.d.ts | 11 ++++++++--- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/readme.md b/readme.md index 55ca665c6..cce29e85d 100644 --- a/readme.md +++ b/readme.md @@ -191,7 +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 a union type. Order is not guaranteed. +- [`LastOfUnion`](source/last-of-union.d.ts) - Return a member of a union type. Order is not guaranteed. ### Type Guard diff --git a/source/exclude-exactly.d.ts b/source/exclude-exactly.d.ts index 4515b56a0..678cb9955 100644 --- a/source/exclude-exactly.d.ts +++ b/source/exclude-exactly.d.ts @@ -4,9 +4,9 @@ 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.) +Return `never` if the first and second arguments are identical. +Return the first argument if not. +(But there's a limitation about union/intersection type. See `IsEqual` in `source/is-equal.d.ts`.) @example ``` @@ -30,7 +30,7 @@ type RO_1 = MatchOrNever<{a: 0}, {readonly a: 0}>; //=> {a: 0} ``` -`unknown` and `never` cases, which easily break equality in type level code base. +`unknown` and `never` cases, which easily break equality in type-level codebase. @example ``` @@ -66,7 +66,9 @@ type MatchOrNever = : A; /** -TypeScript's built-in `Exclude` and `ExcludeStrict` in `type-fest` don't distinguishes key modifiers of objects. +A stricter version of `Exclude` 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 ``` @@ -101,6 +103,8 @@ type ExcludeFromUnknown = ExcludeExactly; type ExcludeFromUnknownArray = ExcludeExactly; //=> unknown[] ``` + +@category Improved Built-in */ export type ExcludeExactly = LastOfUnion extends infer D diff --git a/source/last-of-union.d.ts b/source/last-of-union.d.ts index 0c14870a9..013b5ed2a 100644 --- a/source/last-of-union.d.ts +++ b/source/last-of-union.d.ts @@ -2,12 +2,15 @@ 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 returns `never` if `never` is passed. -Note that this is non-deterministic because the order of union members is not guaranteed. +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 -This can be used to implement a recursive type function that accepts a union type. +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 @@ -30,6 +33,8 @@ type Last = LastOfUnion<1 | 2 | 3>; type LastNever = LastOfUnion; //=> never ``` + +@category Type */ export type LastOfUnion = true extends IsNever From 3b54932fc6335d3ec9100ff8470d7d00360b5dbd Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Fri, 6 Feb 2026 20:26:01 +0900 Subject: [PATCH 09/23] test: `UnionToTuple`, super type test cases --- test-d/union-to-tuple.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test-d/union-to-tuple.ts b/test-d/union-to-tuple.ts index 1dcaec026..48ccfc35d 100644 --- a/test-d/union-to-tuple.ts +++ b/test-d/union-to-tuple.ts @@ -12,5 +12,13 @@ expectType({} as (1 | 2 | 3)); type Options2 = UnionToTuple; expectType({} as (1 | false | true)); +// Different modifiers cases. type DifferentModifiers = {a: 0} | {readonly a: 0} | {a?: 0} | {readonly a?: 0}; expectType[number]>({} as DifferentModifiers); + +// Super type cases. +type UnionSuperType0 = {a: string; b: string} | {a: string}; +expectType({} as UnionToTuple[number]); + +type UnionSuperType1 = {a: 1} | {b: 1} | {[x: string]: number}; +expectType({} as UnionToTuple[number]); From 8b984c9de5f567e76cd693d2fe31eaf76c619824 Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Sat, 7 Feb 2026 05:15:34 +0900 Subject: [PATCH 10/23] test: add test cases --- test-d/exclude-exactly.ts | 6 ++++++ test-d/union-to-tuple.ts | 29 +++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/test-d/exclude-exactly.ts b/test-d/exclude-exactly.ts index 935a30c3b..b34cd91cc 100644 --- a/test-d/exclude-exactly.ts +++ b/test-d/exclude-exactly.ts @@ -37,3 +37,9 @@ expectType<2>({} as ExcludeExactly<0 | 1 | 2, 0 | 1>); expectType({} 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({} 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({} as ExcludeExactly<{a: 0} | {a: 0}, {a: 0}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents + +// Identical Intersection +expectType({} as ExcludeExactly<{a: 0} & {a: 0}, {a: 0}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents diff --git a/test-d/union-to-tuple.ts b/test-d/union-to-tuple.ts index 48ccfc35d..855456e45 100644 --- a/test-d/union-to-tuple.ts +++ b/test-d/union-to-tuple.ts @@ -13,12 +13,37 @@ type Options2 = UnionToTuple; expectType({} as (1 | false | true)); // Different modifiers cases. -type DifferentModifiers = {a: 0} | {readonly a: 0} | {a?: 0} | {readonly a?: 0}; +type DifferentModifiers = {a: 0} | {readonly a: 0}; expectType[number]>({} as DifferentModifiers); +expectType['length']>(2); + +type ReversedDifferentModifiers = {readonly a: 0} | {a: 0}; +expectType[number]>({} as ReversedDifferentModifiers); +expectType['length']>(2); // Super type cases. type UnionSuperType0 = {a: string; b: string} | {a: string}; expectType({} as UnionToTuple[number]); +expectType['length']>(2); + +type ReversedUnionSuperType0 = {a: string} | {a: string; b: string}; +expectType({} as UnionToTuple[number]); +expectType['length']>(2); -type UnionSuperType1 = {a: 1} | {b: 1} | {[x: string]: number}; +type UnionSuperType1 = {a: 1} | {[x: string]: number}; expectType({} as UnionToTuple[number]); +expectType['length']>(2); + +type ReversedUnionSuperType1 = {[x: string]: number} | {a: 1}; +expectType({} as UnionToTuple[number]); +expectType['length']>(2); + +// Identical union cases. +type UnionIdentical = {a: string} | {a: string}; // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +expectType['length']>(1); + +type UnionIdenticalIntersection = {a: string} & {a: string} | {a: string}; // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +expectType['length']>(1); + +type ReversedUnionIdenticalIntersection = {a: string} | {a: string} & {a: string}; // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +expectType['length']>(1); From 4baac7ea71a4d0b55bc5e3aedc30b338d8b5368f Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Sat, 7 Feb 2026 19:43:27 +0900 Subject: [PATCH 11/23] update: refine `ExcludeExactly`, update test-cases. Co-authored-by: Som Shekhar Mukherjee <49264891+som-sm@users.noreply.github.com> --- source/exclude-exactly.d.ts | 107 ++++++++++-------------------------- test-d/exclude-exactly.ts | 13 ++--- 2 files changed, 35 insertions(+), 85 deletions(-) diff --git a/source/exclude-exactly.d.ts b/source/exclude-exactly.d.ts index 678cb9955..06e5ca2b3 100644 --- a/source/exclude-exactly.d.ts +++ b/source/exclude-exactly.d.ts @@ -1,69 +1,7 @@ -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 first and second arguments are identical. -Return the first argument if not. -(But there's a limitation about union/intersection type. See `IsEqual` in `source/is-equal.d.ts`.) - -@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} -``` - -`unknown` and `never` cases, which easily break equality in type-level codebase. - -@example -``` -type E = MatchOrNever; -//=> unknown -type F = MatchOrNever; -//=> never -type G = MatchOrNever; -//=> never -type H = MatchOrNever; -//=> 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 -``` -*/ -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; +import type {If} from './if.d.ts'; +import type {IfNotAnyOrNever} from './internal/type.d.ts'; /** A stricter version of `Exclude` that ensures objects with different key modifiers are not considered identical. @@ -106,19 +44,32 @@ type ExcludeFromUnknownArray = ExcludeExactly; @category Improved Built-in */ -export type ExcludeExactly = - LastOfUnion extends infer D - ? true extends IsNever - ? UnionU - : ExcludeExactly<_ExcludeExactly, _ExcludeExactly> - : never; +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 + >; + +type SimpleIsEqual = + (() => G extends A & G | G ? 1 : 2) extends + (() => G extends B & G | G ? 1 : 2) + ? true + : false; -type _ExcludeExactly = - true extends IsAny - ? never - : true extends IsUnknown - ? never - : UnionU extends unknown // Only for union distribution. - ? MatchOrNever - : never; export {}; diff --git a/test-d/exclude-exactly.ts b/test-d/exclude-exactly.ts index b34cd91cc..e242b0cd1 100644 --- a/test-d/exclude-exactly.ts +++ b/test-d/exclude-exactly.ts @@ -3,6 +3,7 @@ import type {ExcludeExactly} from '../index.d.ts'; expectType({} as ExcludeExactly<0 | 1 | number, '1'>); expectType({} as ExcludeExactly<0 | 1 | number, number>); +expectType<0>({} as ExcludeExactly<0, number>); expectType({} as ExcludeExactly<'0' | '1' | string, '1'>); expectType({} as ExcludeExactly<'0' | '1' | string, string>); @@ -22,15 +23,13 @@ expectType({} as ExcludeExactly); expectType<{a: unknown}>({} as ExcludeExactly<{a: unknown}, {a: number}>); expectType({} as ExcludeExactly); -// `unknown` and `any` exclude themselves. +// `unknown` excludes `unknown`, `any` excludes `any`. expectType({} as ExcludeExactly); -expectType({} as ExcludeExactly); +expectType({} as ExcludeExactly); expectType({} as ExcludeExactly); -expectType({} as ExcludeExactly); - -// `unknown` and `any` exclude other types. -expectType({} as ExcludeExactly); -expectType({} as ExcludeExactly); +expectType({} as ExcludeExactly); +expectType({} as ExcludeExactly); +expectType({} as ExcludeExactly); // Union expectType<2>({} as ExcludeExactly<0 | 1 | 2, 0 | 1>); From 0699dc068fef495dab7db4e3757ce7989c478383 Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Sat, 7 Feb 2026 20:15:47 +0900 Subject: [PATCH 12/23] update: move `SimpleIsEqual` to `internal` --- source/exclude-exactly.d.ts | 8 +-- source/internal/type.d.ts | 24 ++++++++ source/is-equal.d.ts | 12 +--- test-d/internal/simple-is-equal.ts | 95 ++++++++++++++++++++++++++++++ 4 files changed, 123 insertions(+), 16 deletions(-) create mode 100644 test-d/internal/simple-is-equal.ts diff --git a/source/exclude-exactly.d.ts b/source/exclude-exactly.d.ts index 06e5ca2b3..01d8c7b55 100644 --- a/source/exclude-exactly.d.ts +++ b/source/exclude-exactly.d.ts @@ -1,7 +1,7 @@ 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} from './internal/type.d.ts'; +import type {IfNotAnyOrNever, SimpleIsEqual} from './internal/type.d.ts'; /** A stricter version of `Exclude` that ensures objects with different key modifiers are not considered identical. @@ -66,10 +66,4 @@ type _ExcludeExactly = Union, Union >; -type SimpleIsEqual = - (() => G extends A & G | G ? 1 : 2) extends - (() => G extends B & G | G ? 1 : 2) - ? true - : false; - export {}; diff --git a/source/internal/type.d.ts b/source/internal/type.d.ts index c1f53a2b9..26edb6d39 100644 --- a/source/internal/type.d.ts +++ b/source/internal/type.d.ts @@ -161,4 +161,28 @@ export type IsExactOptionalPropertyTypesEnabled = [(string | undefined)?] extend ? false : true; +/** +A simple version of `IsEqual`. + +`SimpleIsEqual` and `SimpleIsEqual` 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 = + (() => G extends A & G | G ? 1 : 2) extends + (() => G extends B & G | G ? 1 : 2) + ? true + : false; + export {}; diff --git a/source/is-equal.d.ts b/source/is-equal.d.ts index b0183048d..0fde0c299 100644 --- a/source/is-equal.d.ts +++ b/source/is-equal.d.ts @@ -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. @@ -28,15 +29,8 @@ type Includes = export type IsEqual = [A] extends [B] ? [B] extends [A] - ? _IsEqual + ? SimpleIsEqual : false : 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) - ? true - : false; - export {}; diff --git a/test-d/internal/simple-is-equal.ts b/test-d/internal/simple-is-equal.ts new file mode 100644 index 000000000..3d0544896 --- /dev/null +++ b/test-d/internal/simple-is-equal.ts @@ -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({} as SimpleIsEqual); +expectType({} as SimpleIsEqual<1, 1>); +expectType({} as SimpleIsEqual<'A', 'B'>); +expectType({} as SimpleIsEqual<'foo', 'foo'>); +expectType({} as SimpleIsEqual); +expectType({} as SimpleIsEqual); + +expectType({} as SimpleIsEqual); +expectType({} as SimpleIsEqual<'', never>); +expectType({} as SimpleIsEqual); +expectType({} as SimpleIsEqual); +expectType({} as SimpleIsEqual); +expectType({} as SimpleIsEqual); +expectType({} as SimpleIsEqual); +// `IsEqual` returns `false`, `SimpleIsEqual` returns `true`. +expectType({} as SimpleIsEqual); +// `IsEqual` returns `false`, `SimpleIsEqual` returns `true`. +expectType({} as SimpleIsEqual); +expectType({} as SimpleIsEqual<[never], [unknown]>); +expectType({} as SimpleIsEqual<[unknown], [never]>); +expectType({} as SimpleIsEqual<[any], [never]>); +expectType({} as SimpleIsEqual<[any], [any]>); +expectType({} as SimpleIsEqual<[never], [never]>); + +expectType({} as SimpleIsEqual<1 | 2, 1>); +expectType({} as SimpleIsEqual<1 | 2, 2 | 3>); +expectType({} as SimpleIsEqual<1 | 2, 2 | 1>); +expectType({} as SimpleIsEqual); + +expectType({} as SimpleIsEqual<{a: 1}, {a: 1}>); +expectType({} as SimpleIsEqual<{a: 1}, {a?: 1}>); +expectType({} as SimpleIsEqual<{a: 1}, {readonly a: 1}>); + +expectType({} as SimpleIsEqual<[], []>); +expectType({} as SimpleIsEqual); +expectType({} as SimpleIsEqual); +expectType({} as SimpleIsEqual); +expectType({} as SimpleIsEqual); +expectType({} as SimpleIsEqual); +expectType({} as SimpleIsEqual<[string], [string]>); +expectType({} as SimpleIsEqual<[string], [string, number]>); +expectType({} as SimpleIsEqual<[0, 1] | [0, 2], [0, 2]>); + +type LongTupleNumber = TupleOf<50, 0>; +expectType({} as SimpleIsEqual); + +type ReadonlyLongTupleNumber = Readonly>; +expectType({} as SimpleIsEqual); + +expectType({} as SimpleIsEqual); + +// Missing all generic parameters. +// @ts-expect-error +type A = SimpleIsEqual; + +// Missing `Y` generic parameter. +// @ts-expect-error +type B = SimpleIsEqual; + +// 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(true); + +type IntersectionType = SimpleIsEqual<{a: 1} & {a: 1}, {a: 1}>; // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +expectType(true); + +// Test for PR https://github.com/sindresorhus/type-fest/pull/1231 +type BranchOnWrappedTupleMatches = (Tpl extends [[0, 2]] ? 'Foo' : 'Bar'); +type BranchOnWrappedTupleDoesNotMatch = (Tpl extends [[0, 1]] ? 'Foo' : 'Bar'); +type BranchOnTupleMatches = (Tpl extends [0, 2] ? 'Foo' : 'Bar'); +type BranchOnTupleDoesNotMatch = (Tpl extends [0, 1] ? 'Foo' : 'Bar'); + +declare const equalWrappedTupleIntersectionToBeNeverAndNever: SimpleIsEqual<(BranchOnWrappedTupleMatches<[[0, 2]]> & BranchOnWrappedTupleDoesNotMatch<[[0, 2]]>), never>; +expectType(equalWrappedTupleIntersectionToBeNeverAndNever); + +// `IsEqual` returns `false`, `SimpleIsEqual` returns `true`. +declare const equalWrappedTupleIntersectionToBeNeverAndNeverExpanded: [0, 2] extends infer Tpl ? SimpleIsEqual<(BranchOnWrappedTupleMatches<[Tpl]> & BranchOnWrappedTupleDoesNotMatch<[Tpl]>), never> : never; +expectType(equalWrappedTupleIntersectionToBeNeverAndNeverExpanded); + +declare const equalTupleIntersectionToBeNeverAndNever: SimpleIsEqual<(BranchOnTupleMatches<[0, 2]> & BranchOnTupleDoesNotMatch<[0, 2]>), never>; +expectType(equalTupleIntersectionToBeNeverAndNever); + +declare const equalTupleIntersectionToBeNeverAndNeverExpanded: [0, 2] extends infer Tpl ? SimpleIsEqual<(BranchOnTupleMatches & BranchOnTupleDoesNotMatch), never> : never; +expectType(equalTupleIntersectionToBeNeverAndNeverExpanded); + +declare const equalTupleIntersectionAndTuple: SimpleIsEqual<[{a: 1}] & [{a: 1}], [{a: 1}]>; // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents +expectType(equalTupleIntersectionAndTuple); + +// Test for Issue https://github.com/sindresorhus/type-fest/issues/1305 +type Assignability> = any; +type TestAssignability = Assignability; From 01cbb49943a4ab4eaf630ae77c60e9acd3946f4b Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Sun, 8 Feb 2026 07:25:20 +0900 Subject: [PATCH 13/23] test: `LastOfUnion` ensures all union members would be picked. --- test-d/last-of-union.ts | 49 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/test-d/last-of-union.ts b/test-d/last-of-union.ts index f774fb9bd..779c96afd 100644 --- a/test-d/last-of-union.ts +++ b/test-d/last-of-union.ts @@ -1,5 +1,5 @@ import {expectType} from 'tsd'; -import type {LastOfUnion, IsAny, IsUnknown} from '../index.d.ts'; +import type {LastOfUnion, IsAny, IsUnknown, IsNever} from '../index.d.ts'; // `LastOfUnion` distinguishes between different modifiers. type UnionType = {a: 0} | {b: 0} | {a?: 0} | {readonly a?: 0} | {readonly a: 0}; @@ -11,3 +11,50 @@ expectType({} as LastOfUnion); expectType({} as IsUnknown>); expectType({} as IsAny>); + +// Ensure a loop of `LastOfUnion` returns all elements. +type UnionToTuple> = +IsNever extends false + ? [...UnionToTuple>, L] + : []; + +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; + +type ExcludeExactly = + LastOfUnion extends infer D + ? true extends IsNever + ? UnionU + : ExcludeExactly<_ExcludeExactly, _ExcludeExactly> + : never; + +type _ExcludeExactly = + true extends IsAny + ? never + : true extends IsUnknown + ? never + : UnionU extends unknown // Only for union distribution. + ? MatchOrNever + : never; + +type DifferentModifierUnion = {readonly a: 0} | {a: 0}; +expectType({} as UnionToTuple[number]); +expectType<2>({} as UnionToTuple['length']); + +type ReversedDifferentModifierUnion = {a: 0} | {readonly a: 0}; +expectType({} as UnionToTuple[number]); +expectType<2>({} as UnionToTuple['length']); + +type SuperTypeUnion = {a: 0; b: 0} | {a: 0}; +expectType({} as UnionToTuple[number]); +expectType<2>({} as UnionToTuple['length']); + +type ReversedSuperTypeUnion = {a: 0} | {a: 0; b: 0}; +expectType({} as UnionToTuple[number]); +expectType<2>({} as UnionToTuple['length']); From 408b2de6d239ca6c8d860d2879c8d290a13c6698 Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Sun, 8 Feb 2026 07:45:04 +0900 Subject: [PATCH 14/23] test: `test-d/last-of-union.ts`, remove `any` and `unknown` checker to return `never` --- test-d/last-of-union.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/test-d/last-of-union.ts b/test-d/last-of-union.ts index 779c96afd..a64b8e224 100644 --- a/test-d/last-of-union.ts +++ b/test-d/last-of-union.ts @@ -35,13 +35,9 @@ type ExcludeExactly = : never; type _ExcludeExactly = - true extends IsAny - ? never - : true extends IsUnknown - ? never - : UnionU extends unknown // Only for union distribution. - ? MatchOrNever - : never; + UnionU extends unknown // Only for union distribution. + ? MatchOrNever + : never; type DifferentModifierUnion = {readonly a: 0} | {a: 0}; expectType({} as UnionToTuple[number]); From 614ad2ac6951d85da072dc7c5425ce28992c7d87 Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Tue, 10 Feb 2026 17:20:06 +0900 Subject: [PATCH 15/23] test: add edge cases. --- test-d/exclude-exactly.ts | 5 +++++ test-d/union-to-tuple.ts | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/test-d/exclude-exactly.ts b/test-d/exclude-exactly.ts index e242b0cd1..1e0c48786 100644 --- a/test-d/exclude-exactly.ts +++ b/test-d/exclude-exactly.ts @@ -16,6 +16,11 @@ expectType({} as ExcludeExactly<{readonly a: 0}, {readonly a: 0}>); expectType<0 | 1 | 2>({} as ExcludeExactly<0 | 1 | 2, never>); expectType({} as ExcludeExactly); +// Edge cases. +expectType({} as ExcludeExactly); +expectType({} as ExcludeExactly); +expectType({} as ExcludeExactly); + // `unknown` cannot be excluded like `unknown\T` in any cases. expectType({} as ExcludeExactly); expectType<[unknown]>({} as ExcludeExactly<[unknown], [number]>); diff --git a/test-d/union-to-tuple.ts b/test-d/union-to-tuple.ts index 855456e45..f62c67b9a 100644 --- a/test-d/union-to-tuple.ts +++ b/test-d/union-to-tuple.ts @@ -12,6 +12,11 @@ expectType({} as (1 | 2 | 3)); type Options2 = UnionToTuple; expectType({} as (1 | false | true)); +// Edge cases. +expectType['length']>(0); +expectType['length']>(1); +expectType['length']>(1); + // Different modifiers cases. type DifferentModifiers = {a: 0} | {readonly a: 0}; expectType[number]>({} as DifferentModifiers); From e4bd1e81b30b86f404c22bed5d802da146d6980f Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Wed, 11 Feb 2026 18:05:25 +0900 Subject: [PATCH 16/23] update: remove `SimpleIsEqual`, move `LastOfUnion` to `internal`, delete unused tests --- index.d.ts | 1 - readme.md | 1 - source/exclude-exactly.d.ts | 43 +++++--------- source/internal/type.d.ts | 45 ++++++++++---- source/is-equal.d.ts | 10 +++- source/last-of-union.d.ts | 46 --------------- source/union-to-tuple.d.ts | 2 +- test-d/exclude-exactly.ts | 11 ++-- test-d/internal/last-of-union.ts | 22 +++++++ test-d/internal/simple-is-equal.ts | 95 ------------------------------ test-d/last-of-union.ts | 56 ------------------ test-d/union-to-tuple.ts | 46 +++------------ 12 files changed, 91 insertions(+), 287 deletions(-) delete mode 100644 source/last-of-union.d.ts create mode 100644 test-d/internal/last-of-union.ts delete mode 100644 test-d/internal/simple-is-equal.ts delete mode 100644 test-d/last-of-union.ts diff --git a/index.d.ts b/index.d.ts index 42afe12b5..d9d9973bf 100644 --- a/index.d.ts +++ b/index.d.ts @@ -167,7 +167,6 @@ 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'; diff --git a/readme.md b/readme.md index cce29e85d..b3c06f052 100644 --- a/readme.md +++ b/readme.md @@ -191,7 +191,6 @@ 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 diff --git a/source/exclude-exactly.d.ts b/source/exclude-exactly.d.ts index 01d8c7b55..d4f10a79c 100644 --- a/source/exclude-exactly.d.ts +++ b/source/exclude-exactly.d.ts @@ -1,22 +1,11 @@ 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'; +import type {IsEqual} from './is-equal.d.ts'; +import type {IfNotAnyOrNever} from './internal/type.d.ts'; /** -A stricter version of `Exclude` 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 -``` +A stricter version of `Exclude` that excludes types only when they are exactly identical. `ExcludeExactly` keeps the union members element if the members are not identical. @@ -24,22 +13,18 @@ type NeverReturned_1 = ExcludeStrict<{a: 0} | {readonly a: 0}, {readonly a: 0}>; ``` 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}>; +type TestExclude1 = Exclude<'a' | 'b' | 'c' | 1 | 2 | 3, string>; +//=> 1 | 2 | 3 +type TestExcludeExactly1 = ExcludeExactly<'a' | 'b' | 'c' | 1 | 2 | 3, string>; +//=> 'a' | 'b' | 'c' | 1 | 2 | 3 +type TestExclude2 = Exclude<'a' | 'b' | 'c' | 1 | 2 | 3, any>; //=> never -type ExcludeSubType = ExcludeExactly<0 | 1 | number, 1>; -//=> number -type ExcludeAllSet = ExcludeExactly<0 | 1 | number, number>; +type TestExcludeExactly2 = ExcludeExactly<'a' | 'b' | 'c' | 1 | 2 | 3, any>; +//=> 'a' | 'b' | 'c' | 1 | 2 | 3 +type TestExclude3 = Exclude<{a: string} | {a: string; b: string}, {a: string}>; //=> never -type ExcludeFromUnknown = ExcludeExactly; -//=> unknown -type ExcludeFromUnknownArray = ExcludeExactly; -//=> unknown[] +type TestExcludeExactly3 = ExcludeExactly<{a: string} | {a: string; b: string}, {a: string}>; +//=> {a: string; b: string} ``` @category Improved Built-in @@ -58,7 +43,7 @@ type _ExcludeExactly = IfNotAnyOrNever, true, never> + ? If, true, never> : never] extends [never] ? Union : never : never, // If `Delete` is `any` or `never`, then return `Union`, diff --git a/source/internal/type.d.ts b/source/internal/type.d.ts index 26edb6d39..27f4bccdd 100644 --- a/source/internal/type.d.ts +++ b/source/internal/type.d.ts @@ -3,6 +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'; /** Matches any primitive, `void`, `Date`, or `RegExp` value. @@ -162,27 +163,45 @@ export type IsExactOptionalPropertyTypesEnabled = [(string | undefined)?] extend : true; /** -A simple version of `IsEqual`. +Return a member of a union type. Order is not guaranteed. +Returns `never` when the input is `never`. -`SimpleIsEqual` and `SimpleIsEqual` return `true`, whereas `IsEqual` returns `false` correctly. +@see https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468375328 -`SimpleIsEqual` doesn't return `false` correctly for identical union/intersection arguments. +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 ``` -type UnionCase = SimpleIsEqual<{a: {b: 0} | {b: 0}}, {a: {b: 0}}>; -//=> false +import type {LastOfUnion, ExcludeExactly, IsNever} from 'type-fest'; -type IntersectionCase = SimpleIsEqual<{a: {b: 0} & {b: 0}}, {a: {b: 0}}>; -//=> false +export type UnionToTuple> = + IsNever extends false + ? [...UnionToTuple>, L] + : []; +``` + +@example ``` +import type {LastOfUnion} from 'type-fest'; -`SimpleIsEqual` fails the `equalWrappedTupleIntersectionToBeNeverAndNeverExpanded` test in `test-d/internal/simple-is-equal.ts`. +type Last = LastOfUnion<1 | 2 | 3>; +//=> 3 + +type LastNever = LastOfUnion; +//=> never +``` + +@category Type */ -export type SimpleIsEqual = - (() => G extends A & G | G ? 1 : 2) extends - (() => G extends B & G | G ? 1 : 2) - ? true - : false; +export type LastOfUnion = + true extends IsNever + ? never + : UnionToIntersection T : never> extends () => (infer R) + ? R + : never; export {}; diff --git a/source/is-equal.d.ts b/source/is-equal.d.ts index 0fde0c299..48e5b047d 100644 --- a/source/is-equal.d.ts +++ b/source/is-equal.d.ts @@ -1,5 +1,3 @@ -import type {SimpleIsEqual} from './internal/type.d.ts'; - /** Returns a boolean for whether the two given types are equal. @@ -29,8 +27,14 @@ type Includes = export type IsEqual = [A] extends [B] ? [B] extends [A] - ? SimpleIsEqual + ? _IsEqual : false : false; +type _IsEqual = + (() => G extends A & G | G ? 1 : 2) extends + (() => G extends B & G | G ? 1 : 2) + ? true + : false; + export {}; diff --git a/source/last-of-union.d.ts b/source/last-of-union.d.ts deleted file mode 100644 index 013b5ed2a..000000000 --- a/source/last-of-union.d.ts +++ /dev/null @@ -1,46 +0,0 @@ -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> = - IsNever extends false - ? [...UnionToTuple>, L] - : []; -``` - -@example -``` -import type {LastOfUnion} from 'type-fest'; - -type Last = LastOfUnion<1 | 2 | 3>; -//=> 3 - -type LastNever = LastOfUnion; -//=> never -``` - -@category Type -*/ -export type LastOfUnion = - true extends IsNever - ? never - : UnionToIntersection T : never> extends () => (infer R) - ? R - : never; - -export {}; diff --git a/source/union-to-tuple.d.ts b/source/union-to-tuple.d.ts index f9184f7d9..8f514eb19 100644 --- a/source/union-to-tuple.d.ts +++ b/source/union-to-tuple.d.ts @@ -1,6 +1,6 @@ import type {IsNever} from './is-never.d.ts'; import type {ExcludeExactly} from './exclude-exactly.d.ts'; -import type {LastOfUnion} from './last-of-union.d.ts'; +import type {LastOfUnion} from './internal/index.d.ts'; /** Convert a union type into an unordered tuple type of its elements. diff --git a/test-d/exclude-exactly.ts b/test-d/exclude-exactly.ts index 1e0c48786..74029a6ed 100644 --- a/test-d/exclude-exactly.ts +++ b/test-d/exclude-exactly.ts @@ -1,11 +1,12 @@ import {expectType} from 'tsd'; -import type {ExcludeExactly} from '../index.d.ts'; +import type {LastOfUnion} from '../source/internal/index.d.ts'; +import type {ExcludeExactly, IsNever} from '../index.d.ts'; -expectType({} as ExcludeExactly<0 | 1 | number, '1'>); -expectType({} as ExcludeExactly<0 | 1 | number, number>); +expectType({} as ExcludeExactly); +expectType({} as ExcludeExactly); expectType<0>({} as ExcludeExactly<0, number>); -expectType({} as ExcludeExactly<'0' | '1' | string, '1'>); -expectType({} as ExcludeExactly<'0' | '1' | string, string>); +expectType({} as ExcludeExactly); +expectType({} as ExcludeExactly); // `{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}>); diff --git a/test-d/internal/last-of-union.ts b/test-d/internal/last-of-union.ts new file mode 100644 index 000000000..5669b2b5b --- /dev/null +++ b/test-d/internal/last-of-union.ts @@ -0,0 +1,22 @@ +import {expectType} from 'tsd'; +import type {LastOfUnion} from '../../source/internal/index.d.ts'; +import type {IsAny, IsUnknown, UnionToTuple} from '../../index.d.ts'; + +// `LastOfUnion` distinguishes between different modifiers. +type UnionType = {a: 0} | {b: 0} | {a?: 0} | {readonly a?: 0} | {readonly a: 0}; +expectType({} as LastOfUnion extends UnionType ? true : false); +expectType({} as UnionType extends LastOfUnion ? true : false); + +// `never` acts as a termination condition with `IsNever`. +expectType({} as LastOfUnion); + +expectType({} as IsUnknown>); +expectType({} as IsAny>); + +type DifferentModifierUnion = {readonly a: 0} | {a: 0}; +expectType({} as UnionToTuple[number]); +expectType<2>({} as UnionToTuple['length']); + +type ReversedDifferentModifierUnion = {a: 0} | {readonly a: 0}; +expectType({} as UnionToTuple[number]); +expectType<2>({} as UnionToTuple['length']); diff --git a/test-d/internal/simple-is-equal.ts b/test-d/internal/simple-is-equal.ts deleted file mode 100644 index 3d0544896..000000000 --- a/test-d/internal/simple-is-equal.ts +++ /dev/null @@ -1,95 +0,0 @@ -import {expectType} from 'tsd'; -import type {TupleOf} from '../../index.d.ts'; -import type {SimpleIsEqual} from '../../source/internal/index.d.ts'; - -expectType({} as SimpleIsEqual); -expectType({} as SimpleIsEqual<1, 1>); -expectType({} as SimpleIsEqual<'A', 'B'>); -expectType({} as SimpleIsEqual<'foo', 'foo'>); -expectType({} as SimpleIsEqual); -expectType({} as SimpleIsEqual); - -expectType({} as SimpleIsEqual); -expectType({} as SimpleIsEqual<'', never>); -expectType({} as SimpleIsEqual); -expectType({} as SimpleIsEqual); -expectType({} as SimpleIsEqual); -expectType({} as SimpleIsEqual); -expectType({} as SimpleIsEqual); -// `IsEqual` returns `false`, `SimpleIsEqual` returns `true`. -expectType({} as SimpleIsEqual); -// `IsEqual` returns `false`, `SimpleIsEqual` returns `true`. -expectType({} as SimpleIsEqual); -expectType({} as SimpleIsEqual<[never], [unknown]>); -expectType({} as SimpleIsEqual<[unknown], [never]>); -expectType({} as SimpleIsEqual<[any], [never]>); -expectType({} as SimpleIsEqual<[any], [any]>); -expectType({} as SimpleIsEqual<[never], [never]>); - -expectType({} as SimpleIsEqual<1 | 2, 1>); -expectType({} as SimpleIsEqual<1 | 2, 2 | 3>); -expectType({} as SimpleIsEqual<1 | 2, 2 | 1>); -expectType({} as SimpleIsEqual); - -expectType({} as SimpleIsEqual<{a: 1}, {a: 1}>); -expectType({} as SimpleIsEqual<{a: 1}, {a?: 1}>); -expectType({} as SimpleIsEqual<{a: 1}, {readonly a: 1}>); - -expectType({} as SimpleIsEqual<[], []>); -expectType({} as SimpleIsEqual); -expectType({} as SimpleIsEqual); -expectType({} as SimpleIsEqual); -expectType({} as SimpleIsEqual); -expectType({} as SimpleIsEqual); -expectType({} as SimpleIsEqual<[string], [string]>); -expectType({} as SimpleIsEqual<[string], [string, number]>); -expectType({} as SimpleIsEqual<[0, 1] | [0, 2], [0, 2]>); - -type LongTupleNumber = TupleOf<50, 0>; -expectType({} as SimpleIsEqual); - -type ReadonlyLongTupleNumber = Readonly>; -expectType({} as SimpleIsEqual); - -expectType({} as SimpleIsEqual); - -// Missing all generic parameters. -// @ts-expect-error -type A = SimpleIsEqual; - -// Missing `Y` generic parameter. -// @ts-expect-error -type B = SimpleIsEqual; - -// 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(true); - -type IntersectionType = SimpleIsEqual<{a: 1} & {a: 1}, {a: 1}>; // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents -expectType(true); - -// Test for PR https://github.com/sindresorhus/type-fest/pull/1231 -type BranchOnWrappedTupleMatches = (Tpl extends [[0, 2]] ? 'Foo' : 'Bar'); -type BranchOnWrappedTupleDoesNotMatch = (Tpl extends [[0, 1]] ? 'Foo' : 'Bar'); -type BranchOnTupleMatches = (Tpl extends [0, 2] ? 'Foo' : 'Bar'); -type BranchOnTupleDoesNotMatch = (Tpl extends [0, 1] ? 'Foo' : 'Bar'); - -declare const equalWrappedTupleIntersectionToBeNeverAndNever: SimpleIsEqual<(BranchOnWrappedTupleMatches<[[0, 2]]> & BranchOnWrappedTupleDoesNotMatch<[[0, 2]]>), never>; -expectType(equalWrappedTupleIntersectionToBeNeverAndNever); - -// `IsEqual` returns `false`, `SimpleIsEqual` returns `true`. -declare const equalWrappedTupleIntersectionToBeNeverAndNeverExpanded: [0, 2] extends infer Tpl ? SimpleIsEqual<(BranchOnWrappedTupleMatches<[Tpl]> & BranchOnWrappedTupleDoesNotMatch<[Tpl]>), never> : never; -expectType(equalWrappedTupleIntersectionToBeNeverAndNeverExpanded); - -declare const equalTupleIntersectionToBeNeverAndNever: SimpleIsEqual<(BranchOnTupleMatches<[0, 2]> & BranchOnTupleDoesNotMatch<[0, 2]>), never>; -expectType(equalTupleIntersectionToBeNeverAndNever); - -declare const equalTupleIntersectionToBeNeverAndNeverExpanded: [0, 2] extends infer Tpl ? SimpleIsEqual<(BranchOnTupleMatches & BranchOnTupleDoesNotMatch), never> : never; -expectType(equalTupleIntersectionToBeNeverAndNeverExpanded); - -declare const equalTupleIntersectionAndTuple: SimpleIsEqual<[{a: 1}] & [{a: 1}], [{a: 1}]>; // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents -expectType(equalTupleIntersectionAndTuple); - -// Test for Issue https://github.com/sindresorhus/type-fest/issues/1305 -type Assignability> = any; -type TestAssignability = Assignability; diff --git a/test-d/last-of-union.ts b/test-d/last-of-union.ts deleted file mode 100644 index a64b8e224..000000000 --- a/test-d/last-of-union.ts +++ /dev/null @@ -1,56 +0,0 @@ -import {expectType} from 'tsd'; -import type {LastOfUnion, IsAny, IsUnknown, IsNever} from '../index.d.ts'; - -// `LastOfUnion` distinguishes between different modifiers. -type UnionType = {a: 0} | {b: 0} | {a?: 0} | {readonly a?: 0} | {readonly a: 0}; -expectType({} as LastOfUnion extends UnionType ? true : false); -expectType({} as UnionType extends LastOfUnion ? true : false); - -// `never` acts as a termination condition with `IsNever`. -expectType({} as LastOfUnion); - -expectType({} as IsUnknown>); -expectType({} as IsAny>); - -// Ensure a loop of `LastOfUnion` returns all elements. -type UnionToTuple> = -IsNever extends false - ? [...UnionToTuple>, L] - : []; - -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; - -type ExcludeExactly = - LastOfUnion extends infer D - ? true extends IsNever - ? UnionU - : ExcludeExactly<_ExcludeExactly, _ExcludeExactly> - : never; - -type _ExcludeExactly = - UnionU extends unknown // Only for union distribution. - ? MatchOrNever - : never; - -type DifferentModifierUnion = {readonly a: 0} | {a: 0}; -expectType({} as UnionToTuple[number]); -expectType<2>({} as UnionToTuple['length']); - -type ReversedDifferentModifierUnion = {a: 0} | {readonly a: 0}; -expectType({} as UnionToTuple[number]); -expectType<2>({} as UnionToTuple['length']); - -type SuperTypeUnion = {a: 0; b: 0} | {a: 0}; -expectType({} as UnionToTuple[number]); -expectType<2>({} as UnionToTuple['length']); - -type ReversedSuperTypeUnion = {a: 0} | {a: 0; b: 0}; -expectType({} as UnionToTuple[number]); -expectType<2>({} as UnionToTuple['length']); diff --git a/test-d/union-to-tuple.ts b/test-d/union-to-tuple.ts index f62c67b9a..9829c6f9e 100644 --- a/test-d/union-to-tuple.ts +++ b/test-d/union-to-tuple.ts @@ -13,42 +13,14 @@ type Options2 = UnionToTuple; expectType({} as (1 | false | true)); // Edge cases. -expectType['length']>(0); -expectType['length']>(1); -expectType['length']>(1); +expectType<[]>({} as UnionToTuple); +expectType<[any]>({} as UnionToTuple); +expectType<[unknown]>({} as UnionToTuple); -// Different modifiers cases. -type DifferentModifiers = {a: 0} | {readonly a: 0}; -expectType[number]>({} as DifferentModifiers); -expectType['length']>(2); +type DifferentModifierUnion = {readonly a: 0} | {a: 0}; +expectType({} as UnionToTuple[number]); +expectType<2>({} as UnionToTuple['length']); -type ReversedDifferentModifiers = {readonly a: 0} | {a: 0}; -expectType[number]>({} as ReversedDifferentModifiers); -expectType['length']>(2); - -// Super type cases. -type UnionSuperType0 = {a: string; b: string} | {a: string}; -expectType({} as UnionToTuple[number]); -expectType['length']>(2); - -type ReversedUnionSuperType0 = {a: string} | {a: string; b: string}; -expectType({} as UnionToTuple[number]); -expectType['length']>(2); - -type UnionSuperType1 = {a: 1} | {[x: string]: number}; -expectType({} as UnionToTuple[number]); -expectType['length']>(2); - -type ReversedUnionSuperType1 = {[x: string]: number} | {a: 1}; -expectType({} as UnionToTuple[number]); -expectType['length']>(2); - -// Identical union cases. -type UnionIdentical = {a: string} | {a: string}; // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents -expectType['length']>(1); - -type UnionIdenticalIntersection = {a: string} & {a: string} | {a: string}; // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents -expectType['length']>(1); - -type ReversedUnionIdenticalIntersection = {a: string} | {a: string} & {a: string}; // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents -expectType['length']>(1); +type ReversedDifferentModifierUnion = {a: 0} | {readonly a: 0}; +expectType({} as UnionToTuple[number]); +expectType<2>({} as UnionToTuple['length']); From 65cc0bd738a886750e072b9325f4b1cc6ecb1a88 Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Fri, 13 Feb 2026 02:41:31 +0900 Subject: [PATCH 17/23] refactor: remove unused imports and tests --- test-d/exclude-exactly.ts | 9 +-------- test-d/union-to-tuple.ts | 3 +++ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/test-d/exclude-exactly.ts b/test-d/exclude-exactly.ts index 74029a6ed..43c1c88b5 100644 --- a/test-d/exclude-exactly.ts +++ b/test-d/exclude-exactly.ts @@ -1,6 +1,5 @@ import {expectType} from 'tsd'; -import type {LastOfUnion} from '../source/internal/index.d.ts'; -import type {ExcludeExactly, IsNever} from '../index.d.ts'; +import type {ExcludeExactly} from '../index.d.ts'; expectType({} as ExcludeExactly); expectType({} as ExcludeExactly); @@ -42,9 +41,3 @@ expectType<2>({} as ExcludeExactly<0 | 1 | 2, 0 | 1>); expectType({} 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({} 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({} as ExcludeExactly<{a: 0} | {a: 0}, {a: 0}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents - -// Identical Intersection -expectType({} as ExcludeExactly<{a: 0} & {a: 0}, {a: 0}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents diff --git a/test-d/union-to-tuple.ts b/test-d/union-to-tuple.ts index 9829c6f9e..bfc5386ee 100644 --- a/test-d/union-to-tuple.ts +++ b/test-d/union-to-tuple.ts @@ -17,10 +17,13 @@ expectType<[]>({} as UnionToTuple); expectType<[any]>({} as UnionToTuple); expectType<[unknown]>({} as UnionToTuple); +// Test for https://github.com/sindresorhus/type-fest/issues/1352 type DifferentModifierUnion = {readonly a: 0} | {a: 0}; expectType({} as UnionToTuple[number]); expectType<2>({} as UnionToTuple['length']); +// Note: Union order is not guaranteed, but this test is still valuable. +// https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468375328 type ReversedDifferentModifierUnion = {a: 0} | {readonly a: 0}; expectType({} as UnionToTuple[number]); expectType<2>({} as UnionToTuple['length']); From 68c4798d3996fcca6b19f20a5b159c0ea0a0375e Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee Date: Fri, 13 Feb 2026 11:42:22 +0530 Subject: [PATCH 18/23] doc: cleanup JSDoc --- source/exclude-exactly.d.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/source/exclude-exactly.d.ts b/source/exclude-exactly.d.ts index d4f10a79c..5ab98c4fa 100644 --- a/source/exclude-exactly.d.ts +++ b/source/exclude-exactly.d.ts @@ -7,22 +7,25 @@ import type {IfNotAnyOrNever} from './internal/type.d.ts'; /** A stricter version of `Exclude` that excludes types only when they are exactly identical. -`ExcludeExactly` keeps the union members element if the members are not identical. - @example ``` import type {ExcludeExactly} from 'type-fest'; type TestExclude1 = Exclude<'a' | 'b' | 'c' | 1 | 2 | 3, string>; //=> 1 | 2 | 3 + type TestExcludeExactly1 = ExcludeExactly<'a' | 'b' | 'c' | 1 | 2 | 3, string>; //=> 'a' | 'b' | 'c' | 1 | 2 | 3 + type TestExclude2 = Exclude<'a' | 'b' | 'c' | 1 | 2 | 3, any>; //=> never + type TestExcludeExactly2 = ExcludeExactly<'a' | 'b' | 'c' | 1 | 2 | 3, any>; //=> 'a' | 'b' | 'c' | 1 | 2 | 3 + type TestExclude3 = Exclude<{a: string} | {a: string; b: string}, {a: string}>; //=> never + type TestExcludeExactly3 = ExcludeExactly<{a: string} | {a: string; b: string}, {a: string}>; //=> {a: string; b: string} ``` From 78fcde9e16ed3a4e6804659bf6cab1d684328891 Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee Date: Fri, 13 Feb 2026 11:44:07 +0530 Subject: [PATCH 19/23] fix: README description --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index b3c06f052..8d76982db 100644 --- a/readme.md +++ b/readme.md @@ -319,7 +319,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` that ensures every member of `U` can successfully extract something from `T`. - [`ExcludeStrict`](source/exclude-strict.d.ts) - A stricter version of `Exclude` that ensures every member of `U` can successfully exclude something from `T`. -- [`ExcludeExactly`](source/exclude-exactly.d.ts) - A stricter version of `Exclude` that ensures objects with different key modifiers are not considered identical. +- [`ExcludeExactly`](source/exclude-exactly.d.ts) - A stricter version of `Exclude` that excludes types only when they are exactly identical. ## Declined types From 68228349422b52160afe4de987f05a5ff95b3bdb Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee Date: Fri, 13 Feb 2026 11:56:28 +0530 Subject: [PATCH 20/23] test: improve `ExcludeExactly` tests --- test-d/exclude-exactly.ts | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/test-d/exclude-exactly.ts b/test-d/exclude-exactly.ts index 43c1c88b5..4df36e8a9 100644 --- a/test-d/exclude-exactly.ts +++ b/test-d/exclude-exactly.ts @@ -6,38 +6,46 @@ expectType({} as ExcludeExactly); expectType<0>({} as ExcludeExactly<0, number>); expectType({} as ExcludeExactly); expectType({} as ExcludeExactly); +expectType<'0'>({} as ExcludeExactly<'0', 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<{readonly a: 0}>({} as ExcludeExactly<{a: 0} | {readonly a: 0}, {a: 0}>); expectType({} as ExcludeExactly<{readonly a: 0}, {readonly a: 0}>); -// `never` does nothing. +// `never` excludes nothing expectType<0 | 1 | 2>({} as ExcludeExactly<0 | 1 | 2, never>); expectType({} as ExcludeExactly); - -// Edge cases. -expectType({} as ExcludeExactly); expectType({} as ExcludeExactly); expectType({} as ExcludeExactly); -// `unknown` cannot be excluded like `unknown\T` in any cases. +// Excluding from `unknown`/`any` expectType({} as ExcludeExactly); expectType<[unknown]>({} as ExcludeExactly<[unknown], [number]>); expectType({} as ExcludeExactly); expectType<{a: unknown}>({} as ExcludeExactly<{a: unknown}, {a: number}>); expectType({} as ExcludeExactly); +expectType({} as ExcludeExactly); +expectType<[any]>({} as ExcludeExactly<[any], [number]>); +expectType({} as ExcludeExactly); +expectType<{a: any}>({} as ExcludeExactly<{a: any}, {a: number}>); +expectType({} as ExcludeExactly); -// `unknown` excludes `unknown`, `any` excludes `any`. +// Excluding `unknown`/`any` expectType({} as ExcludeExactly); -expectType({} as ExcludeExactly); expectType({} as ExcludeExactly); +expectType({} as ExcludeExactly); expectType({} as ExcludeExactly); expectType({} as ExcludeExactly); expectType({} as ExcludeExactly); +expectType({} as ExcludeExactly); +expectType({} as ExcludeExactly); -// Union +// Unions expectType<2>({} as ExcludeExactly<0 | 1 | 2, 0 | 1>); expectType({} 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({} as ExcludeExactly<{a: 0} | {readonly a: 0} | {a?: 0} | {readonly a?: 0}, {a: 0} | {readonly a: 0} | {a?: 0} | {readonly a?: 0}>); +expectType<{readonly a?: 0}>({} as ExcludeExactly< + {a: 0} | {readonly a: 0} | {a?: 0} | {readonly a?: 0}, {a: 0} | {readonly a: 0} | {a?: 0} +>); +expectType({} as ExcludeExactly< + {a: 0} | {readonly a: 0} | {a?: 0} | {readonly a?: 0}, {a: 0} | {readonly a: 0} | {a?: 0} | {readonly a?: 0} +>); From 5dce64ed978912ce7d87a75eff872db1a1313f21 Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee Date: Fri, 13 Feb 2026 12:13:25 +0530 Subject: [PATCH 21/23] test: add note regarding `DifferentModifierUnion` --- test-d/union-to-tuple.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test-d/union-to-tuple.ts b/test-d/union-to-tuple.ts index bfc5386ee..44648e444 100644 --- a/test-d/union-to-tuple.ts +++ b/test-d/union-to-tuple.ts @@ -18,6 +18,9 @@ expectType<[any]>({} as UnionToTuple); expectType<[unknown]>({} as UnionToTuple); // Test for https://github.com/sindresorhus/type-fest/issues/1352 +// This union is special because `{readonly a: 0}` extends `{a: 0}`, and `{a: 0}` also extends `{readonly a: 0}`, +// meaning both types are assignable to each other. +// See [this comment](https://github.com/sindresorhus/type-fest/pull/1349#issuecomment-3858719735) for more details. type DifferentModifierUnion = {readonly a: 0} | {a: 0}; expectType({} as UnionToTuple[number]); expectType<2>({} as UnionToTuple['length']); From 95798257945e8226e967b270722c92d78103671b Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Sat, 14 Feb 2026 07:25:31 +0900 Subject: [PATCH 22/23] refactor: revert `LastOfUnion` and `IsEqual` comment, delete unused tests in `UnionToTuple` --- source/internal/type.d.ts | 42 ------------------------------ source/is-equal.d.ts | 1 + source/union-to-tuple.d.ts | 44 +++++++++++++++++++++++++++++++- test-d/internal/last-of-union.ts | 22 ---------------- test-d/union-to-tuple.ts | 15 +++-------- 5 files changed, 48 insertions(+), 76 deletions(-) delete mode 100644 test-d/internal/last-of-union.ts diff --git a/source/internal/type.d.ts b/source/internal/type.d.ts index 27f4bccdd..377c456cb 100644 --- a/source/internal/type.d.ts +++ b/source/internal/type.d.ts @@ -162,46 +162,4 @@ export type IsExactOptionalPropertyTypesEnabled = [(string | undefined)?] extend ? false : true; -/** -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> = - IsNever extends false - ? [...UnionToTuple>, L] - : []; -``` - -@example -``` -import type {LastOfUnion} from 'type-fest'; - -type Last = LastOfUnion<1 | 2 | 3>; -//=> 3 - -type LastNever = LastOfUnion; -//=> never -``` - -@category Type -*/ -export type LastOfUnion = - true extends IsNever - ? never - : UnionToIntersection T : never> extends () => (infer R) - ? R - : never; - export {}; diff --git a/source/is-equal.d.ts b/source/is-equal.d.ts index 48e5b047d..306b4687e 100644 --- a/source/is-equal.d.ts +++ b/source/is-equal.d.ts @@ -31,6 +31,7 @@ export type IsEqual = : false : 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/source/union-to-tuple.d.ts b/source/union-to-tuple.d.ts index 8f514eb19..0c0ee0335 100644 --- a/source/union-to-tuple.d.ts +++ b/source/union-to-tuple.d.ts @@ -1,6 +1,48 @@ import type {IsNever} from './is-never.d.ts'; import type {ExcludeExactly} from './exclude-exactly.d.ts'; -import type {LastOfUnion} from './internal/index.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> = + IsNever extends false + ? [...UnionToTuple>, L] + : []; +``` + +@example +``` +import type {LastOfUnion} from 'type-fest'; + +type Last = LastOfUnion<1 | 2 | 3>; +//=> 3 + +type LastNever = LastOfUnion; +//=> never +``` + +@category Type +*/ +type LastOfUnion = + true extends IsNever + ? never + : UnionToIntersection T : never> extends () => (infer R) + ? R + : never; /** Convert a union type into an unordered tuple type of its elements. diff --git a/test-d/internal/last-of-union.ts b/test-d/internal/last-of-union.ts deleted file mode 100644 index 5669b2b5b..000000000 --- a/test-d/internal/last-of-union.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {expectType} from 'tsd'; -import type {LastOfUnion} from '../../source/internal/index.d.ts'; -import type {IsAny, IsUnknown, UnionToTuple} from '../../index.d.ts'; - -// `LastOfUnion` distinguishes between different modifiers. -type UnionType = {a: 0} | {b: 0} | {a?: 0} | {readonly a?: 0} | {readonly a: 0}; -expectType({} as LastOfUnion extends UnionType ? true : false); -expectType({} as UnionType extends LastOfUnion ? true : false); - -// `never` acts as a termination condition with `IsNever`. -expectType({} as LastOfUnion); - -expectType({} as IsUnknown>); -expectType({} as IsAny>); - -type DifferentModifierUnion = {readonly a: 0} | {a: 0}; -expectType({} as UnionToTuple[number]); -expectType<2>({} as UnionToTuple['length']); - -type ReversedDifferentModifierUnion = {a: 0} | {readonly a: 0}; -expectType({} as UnionToTuple[number]); -expectType<2>({} as UnionToTuple['length']); diff --git a/test-d/union-to-tuple.ts b/test-d/union-to-tuple.ts index 44648e444..7a2eb51e4 100644 --- a/test-d/union-to-tuple.ts +++ b/test-d/union-to-tuple.ts @@ -12,21 +12,14 @@ expectType({} as (1 | 2 | 3)); type Options2 = UnionToTuple; expectType({} as (1 | false | true)); -// Edge cases. -expectType<[]>({} as UnionToTuple); -expectType<[any]>({} as UnionToTuple); -expectType<[unknown]>({} as UnionToTuple); - // Test for https://github.com/sindresorhus/type-fest/issues/1352 // This union is special because `{readonly a: 0}` extends `{a: 0}`, and `{a: 0}` also extends `{readonly a: 0}`, // meaning both types are assignable to each other. // See [this comment](https://github.com/sindresorhus/type-fest/pull/1349#issuecomment-3858719735) for more details. type DifferentModifierUnion = {readonly a: 0} | {a: 0}; expectType({} as UnionToTuple[number]); -expectType<2>({} as UnionToTuple['length']); -// Note: Union order is not guaranteed, but this test is still valuable. -// https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468375328 -type ReversedDifferentModifierUnion = {a: 0} | {readonly a: 0}; -expectType({} as UnionToTuple[number]); -expectType<2>({} as UnionToTuple['length']); +// Edge cases. +expectType<[]>({} as UnionToTuple); +expectType<[any]>({} as UnionToTuple); +expectType<[unknown]>({} as UnionToTuple); From 2d728ee6b7f55a47a4761d185ddf34ec8c25c330 Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee Date: Sun, 15 Feb 2026 19:22:02 +0530 Subject: [PATCH 23/23] chore: remove `LastOfUnion` changes --- source/union-to-tuple.d.ts | 42 ++++++-------------------------------- 1 file changed, 6 insertions(+), 36 deletions(-) diff --git a/source/union-to-tuple.d.ts b/source/union-to-tuple.d.ts index 0c0ee0335..a8734077a 100644 --- a/source/union-to-tuple.d.ts +++ b/source/union-to-tuple.d.ts @@ -1,48 +1,20 @@ -import type {IsNever} from './is-never.d.ts'; import type {ExcludeExactly} from './exclude-exactly.d.ts'; +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> = - IsNever extends false - ? [...UnionToTuple>, L] - : []; -``` +Returns the last element of a union type. @example ``` -import type {LastOfUnion} from 'type-fest'; - type Last = LastOfUnion<1 | 2 | 3>; //=> 3 - -type LastNever = LastOfUnion; -//=> never ``` - -@category Type */ type LastOfUnion = - true extends IsNever - ? never - : UnionToIntersection T : never> extends () => (infer R) - ? R - : never; +UnionToIntersection T : never> extends () => (infer R) + ? R + : never; /** Convert a union type into an unordered tuple type of its elements. @@ -81,9 +53,7 @@ const petList = Object.keys(pets) as UnionToTuple; */ export type UnionToTuple> = IsNever extends false - ? ExcludeExactly extends infer E // Improve performance. - ? [...UnionToTuple, L] - : never // Unreachable. + ? [...UnionToTuple>, L] : []; export {};