From f0dfb6acfc6ce28df9d8945274d5c4b02223b12d Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Sat, 14 Feb 2026 17:25:53 +0900 Subject: [PATCH 01/18] feat: export `LastOfUnion` --- index.d.ts | 1 + readme.md | 1 + source/last-of-union.d.ts | 45 ++++++++++++++++++++++++++++++++++++++ source/union-to-tuple.d.ts | 16 +------------- test-d/last-of-union.ts | 43 ++++++++++++++++++++++++++++++++++++ 5 files changed, 91 insertions(+), 15 deletions(-) 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..94a7b6b63 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 member of a 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..6a63d2384 --- /dev/null +++ b/source/last-of-union.d.ts @@ -0,0 +1,45 @@ +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, 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 = + [T] extends [never] + ? 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 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. diff --git a/test-d/last-of-union.ts b/test-d/last-of-union.ts new file mode 100644 index 000000000..a722835e1 --- /dev/null +++ b/test-d/last-of-union.ts @@ -0,0 +1,43 @@ +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]); From 8f776de0c0931af1c884d5a89a1a8e653b547f0b Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Sun, 15 Feb 2026 03:07:22 +0700 Subject: [PATCH 02/18] Update last-of-union.d.ts --- source/last-of-union.d.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/source/last-of-union.d.ts b/source/last-of-union.d.ts index 6a63d2384..05b9939d7 100644 --- a/source/last-of-union.d.ts +++ b/source/last-of-union.d.ts @@ -1,7 +1,8 @@ import type {UnionToIntersection} from './union-to-intersection.d.ts'; /** -Return a member of a union type. Order is not guaranteed. +Returns 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 @@ -16,7 +17,7 @@ It can detect a termination case using {@link IsNever `IsNever`}. ``` import type {LastOfUnion, IsNever} from 'type-fest'; -export type UnionToTuple> = +type UnionToTuple> = IsNever extends false ? [...UnionToTuple>, L] : []; From 596d6db039ac8df1526d10cd1eeb47198c16f3dc Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Sun, 15 Feb 2026 09:26:37 +0900 Subject: [PATCH 03/18] Update readme.md Co-authored-by: Sindre Sorhus --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 94a7b6b63..ca0e73b74 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 member of a union type. Order is not guaranteed. +- [`LastOfUnion`](source/last-of-union.d.ts) - Returns a member of a union type. Order is not guaranteed. ### Type Guard From cb52bd9d9b7299725b9b3881fb06f68f327b17b0 Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Sun, 15 Feb 2026 09:27:52 +0900 Subject: [PATCH 04/18] Update test-d/last-of-union.ts Co-authored-by: Sindre Sorhus --- test-d/last-of-union.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test-d/last-of-union.ts b/test-d/last-of-union.ts index a722835e1..4ce94fb69 100644 --- a/test-d/last-of-union.ts +++ b/test-d/last-of-union.ts @@ -12,6 +12,12 @@ expectType({} as LastOfUnion); expectType({} as IsUnknown>); expectType({} as IsAny>); +type UnionToTupleWithExclude> = + IsNever extends false + ? [...UnionToTupleWithExclude>, L] + : []; + +expectType<1 | 2 | 3>({} as UnionToTupleWithExclude<1 | 2 | 3>[number]); // Ensure a loop of `LastOfUnion` returns all elements. type UnionToTuple> = IsNever extends false From 68c7e285e0efb9dcdef4979e08f9a9a4a70a256e Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Sun, 15 Feb 2026 09:27:59 +0900 Subject: [PATCH 05/18] Update test-d/last-of-union.ts Co-authored-by: Sindre Sorhus --- test-d/last-of-union.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-d/last-of-union.ts b/test-d/last-of-union.ts index 4ce94fb69..6ddbc4b76 100644 --- a/test-d/last-of-union.ts +++ b/test-d/last-of-union.ts @@ -27,8 +27,8 @@ IsNever extends false 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. + // The equality comparison below does not work when `A` is `unknown` and `B` is `never`. + // This branch handles that case. : (() => G extends A & G | G ? 1 : 2) extends (() => G extends B & G | G ? 1 : 2) ? never : A; From ee67eccad4c641fedc5c04b6e61ba30693f59c34 Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Sun, 15 Feb 2026 09:42:32 +0900 Subject: [PATCH 06/18] refactor: add new line --- test-d/last-of-union.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test-d/last-of-union.ts b/test-d/last-of-union.ts index 6ddbc4b76..2e57adbb0 100644 --- a/test-d/last-of-union.ts +++ b/test-d/last-of-union.ts @@ -18,6 +18,7 @@ type UnionToTupleWithExclude> = : []; expectType<1 | 2 | 3>({} as UnionToTupleWithExclude<1 | 2 | 3>[number]); + // Ensure a loop of `LastOfUnion` returns all elements. type UnionToTuple> = IsNever extends false From 818f8a328b618d2421bb8b1e37fc19a9dfead583 Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Sun, 15 Feb 2026 22:24:50 +0900 Subject: [PATCH 07/18] refactor: use `IsNever`, remove unused tests --- source/last-of-union.d.ts | 25 ++++++++++++----------- test-d/last-of-union.ts | 43 ++++----------------------------------- 2 files changed, 17 insertions(+), 51 deletions(-) diff --git a/source/last-of-union.d.ts b/source/last-of-union.d.ts index 05b9939d7..d09a2a29e 100644 --- a/source/last-of-union.d.ts +++ b/source/last-of-union.d.ts @@ -1,10 +1,22 @@ import type {UnionToIntersection} from './union-to-intersection.d.ts'; +import type {IsNever} from './is-never.d.ts'; /** Returns a member of a union type. Order is not guaranteed. Returns `never` when the input is `never`. +@example +``` +import type {LastOfUnion} from 'type-fest'; + +type Last = LastOfUnion<1 | 2 | 3>; +//=> 3 + +type LastNever = LastOfUnion; +//=> never +``` + @see https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468375328 Use-cases: @@ -23,21 +35,10 @@ type UnionToTuple> = : []; ``` -@example -``` -import type {LastOfUnion} from 'type-fest'; - -type Last = LastOfUnion<1 | 2 | 3>; -//=> 3 - -type LastNever = LastOfUnion; -//=> never -``` - @category Type */ export type LastOfUnion = - [T] extends [never] + IsNever extends true ? never : UnionToIntersection T : never> extends () => (infer R) ? R diff --git a/test-d/last-of-union.ts b/test-d/last-of-union.ts index 2e57adbb0..06d7abc2a 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, IsNever} from '../index.d.ts'; +import type {LastOfUnion, UnionToTuple} from '../index.d.ts'; // `LastOfUnion` distinguishes between different modifiers. type UnionType = {a: 0} | {b: 0} | {a?: 0} | {readonly a?: 0} | {readonly a: 0}; @@ -9,42 +9,7 @@ expectType({} as UnionType extends LastOfUnion ? true : false) // `never` acts as a termination condition with `IsNever`. expectType({} as LastOfUnion); -expectType({} as IsUnknown>); -expectType({} as IsAny>); +expectType({} as LastOfUnion); +expectType({} as LastOfUnion); -type UnionToTupleWithExclude> = - IsNever extends false - ? [...UnionToTupleWithExclude>, L] - : []; - -expectType<1 | 2 | 3>({} as UnionToTupleWithExclude<1 | 2 | 3>[number]); - -// Ensure a loop of `LastOfUnion` returns all elements. -type UnionToTuple> = -IsNever extends false - ? [...UnionToTuple>, L] - : []; - -type MatchOrNever = - [unknown, B] extends [A, never] - ? A - // The equality comparison below does not work when `A` is `unknown` and `B` is `never`. - // This branch handles that case. - : (() => 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<1 | 2 | 3>({} as UnionToTuple<1 | 2 | 3>[number]); From b7d661687616e0fc597b8af7576e24c87e7de900 Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Sun, 15 Feb 2026 23:13:57 +0900 Subject: [PATCH 08/18] refactor: rename `LastOfUnion` to `UnionMember` --- index.d.ts | 2 +- readme.md | 4 +++- source/{last-of-union.d.ts => union-member.d.ts} | 12 ++++++------ source/union-to-tuple.d.ts | 4 ++-- test-d/last-of-union.ts | 15 --------------- test-d/union-member.ts | 15 +++++++++++++++ 6 files changed, 27 insertions(+), 25 deletions(-) rename source/{last-of-union.d.ts => union-member.d.ts} (77%) delete mode 100644 test-d/last-of-union.ts create mode 100644 test-d/union-member.ts diff --git a/index.d.ts b/index.d.ts index f859bce03..a2f7b2c1e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -167,7 +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'; +export type {UnionMember} from './source/union-member.d.ts'; // Template literal types export type {CamelCase, CamelCaseOptions} from './source/camel-case.d.ts'; diff --git a/readme.md b/readme.md index ca0e73b74..b9b2ac046 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) - Returns a member of a union type. Order is not guaranteed. +- [`UnionMember`](source/union-member.d.ts) - Returns a member of a union type. Order is not guaranteed. ### Type Guard @@ -356,6 +356,8 @@ Click the type names for complete docs. - `IfAny`, `IfNever`, `If*` - See [`If`](source/if.d.ts) - `MaybePromise` - See [`Promisable`](source/promisable.d.ts) - `ReadonlyTuple` - See [`TupleOf`](source/tuple-of.d.ts) +- `LastOfUnion` - See [`UnionMember`](source/union-member.d.ts) +- `FirstOfUnion` - See [`UnionMember`](source/union-member.d.ts) ## Tips diff --git a/source/last-of-union.d.ts b/source/union-member.d.ts similarity index 77% rename from source/last-of-union.d.ts rename to source/union-member.d.ts index d09a2a29e..3670137b5 100644 --- a/source/last-of-union.d.ts +++ b/source/union-member.d.ts @@ -8,12 +8,12 @@ Returns `never` when the input is `never`. @example ``` -import type {LastOfUnion} from 'type-fest'; +import type {UnionMember} from 'type-fest'; -type Last = LastOfUnion<1 | 2 | 3>; +type Last = UnionMember<1 | 2 | 3>; //=> 3 -type LastNever = LastOfUnion; +type LastNever = UnionMember; //=> never ``` @@ -27,9 +27,9 @@ It can detect a termination case using {@link IsNever `IsNever`}. @example ``` -import type {LastOfUnion, IsNever} from 'type-fest'; +import type {UnionMember, IsNever} from 'type-fest'; -type UnionToTuple> = +type UnionToTuple> = IsNever extends false ? [...UnionToTuple>, L] : []; @@ -37,7 +37,7 @@ type UnionToTuple> = @category Type */ -export type LastOfUnion = +export type UnionMember = IsNever extends true ? never : UnionToIntersection T : never> extends () => (infer R) diff --git a/source/union-to-tuple.d.ts b/source/union-to-tuple.d.ts index 98ab73cf3..9ef76be3d 100644 --- a/source/union-to-tuple.d.ts +++ b/source/union-to-tuple.d.ts @@ -1,5 +1,5 @@ import type {IsNever} from './is-never.d.ts'; -import type {LastOfUnion} from './last-of-union.d.ts'; +import type {UnionMember} from './union-member.d.ts'; /** Convert a union type into an unordered tuple type of its elements. @@ -36,7 +36,7 @@ const petList = Object.keys(pets) as UnionToTuple; @category Array */ -export type UnionToTuple> = +export type UnionToTuple> = IsNever extends false ? [...UnionToTuple>, L] : []; diff --git a/test-d/last-of-union.ts b/test-d/last-of-union.ts deleted file mode 100644 index 06d7abc2a..000000000 --- a/test-d/last-of-union.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {expectType} from 'tsd'; -import type {LastOfUnion, 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 LastOfUnion); -expectType({} as LastOfUnion); - -expectType<1 | 2 | 3>({} as UnionToTuple<1 | 2 | 3>[number]); diff --git a/test-d/union-member.ts b/test-d/union-member.ts new file mode 100644 index 000000000..1f8a4cead --- /dev/null +++ b/test-d/union-member.ts @@ -0,0 +1,15 @@ +import {expectType} from 'tsd'; +import type {UnionMember, UnionToTuple} from '../index.d.ts'; + +// `UnionMember` distinguishes between different modifiers. +type UnionType = {a: 0} | {b: 0} | {a?: 0} | {readonly a?: 0} | {readonly a: 0}; +expectType({} as UnionMember extends UnionType ? true : false); +expectType({} as UnionType extends UnionMember ? true : false); + +// `never` acts as a termination condition with `IsNever`. +expectType({} as UnionMember); + +expectType({} as UnionMember); +expectType({} as UnionMember); + +expectType<1 | 2 | 3>({} as UnionToTuple<1 | 2 | 3>[number]); From 085d1d51a16b84b5e21a260028074ae9ee720e51 Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Sun, 15 Feb 2026 23:31:58 +0900 Subject: [PATCH 09/18] test: add UnionToTupleWithExclude instead of import UnionMember --- test-d/union-member.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test-d/union-member.ts b/test-d/union-member.ts index 1f8a4cead..1d0f019f3 100644 --- a/test-d/union-member.ts +++ b/test-d/union-member.ts @@ -1,5 +1,5 @@ import {expectType} from 'tsd'; -import type {UnionMember, UnionToTuple} from '../index.d.ts'; +import type {UnionMember, IsNever} from '../index.d.ts'; // `UnionMember` distinguishes between different modifiers. type UnionType = {a: 0} | {b: 0} | {a?: 0} | {readonly a?: 0} | {readonly a: 0}; @@ -12,4 +12,8 @@ expectType({} as UnionMember); expectType({} as UnionMember); expectType({} as UnionMember); -expectType<1 | 2 | 3>({} as UnionToTuple<1 | 2 | 3>[number]); +type UnionToTupleWithExclude> = + IsNever extends false + ? [...UnionToTupleWithExclude>, L] + : []; +expectType<1 | 2 | 3>({} as UnionToTupleWithExclude<1 | 2 | 3>[number]); From 89c0f066d495f38fc28b2bb6045f4f3154580e42 Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Sun, 15 Feb 2026 23:37:59 +0900 Subject: [PATCH 10/18] test: ensure pick only one member --- test-d/union-member.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test-d/union-member.ts b/test-d/union-member.ts index 1d0f019f3..39a34d968 100644 --- a/test-d/union-member.ts +++ b/test-d/union-member.ts @@ -12,8 +12,9 @@ expectType({} as UnionMember); expectType({} as UnionMember); expectType({} as UnionMember); +// Ensure exactly one member is selected at a time, while covering all members in the union. type UnionToTupleWithExclude> = IsNever extends false - ? [...UnionToTupleWithExclude>, L] - : []; -expectType<1 | 2 | 3>({} as UnionToTupleWithExclude<1 | 2 | 3>[number]); + ? UnionToTupleWithExclude> | [L] + : never; +expectType<[1] | [2] | [3]>({} as UnionToTupleWithExclude<1 | 2 | 3>); From 4ced771b45ac763417279db692184e99d221f2ac Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Mon, 2 Mar 2026 00:54:37 +0900 Subject: [PATCH 11/18] doc: update comments --- source/union-member.d.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/source/union-member.d.ts b/source/union-member.d.ts index 3670137b5..e103f6cdb 100644 --- a/source/union-member.d.ts +++ b/source/union-member.d.ts @@ -2,7 +2,7 @@ import type {UnionToIntersection} from './union-to-intersection.d.ts'; import type {IsNever} from './is-never.d.ts'; /** -Returns a member of a union type. Order is not guaranteed. +Returns an arbitrary member of a union type. Returns `never` when the input is `never`. @@ -17,14 +17,6 @@ type LastNever = UnionMember; //=> 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 {UnionMember, IsNever} from 'type-fest'; From 1c642f44694a918b46ec13acb548149b33aa1f2d Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Mon, 2 Mar 2026 00:59:23 +0900 Subject: [PATCH 12/18] test: fix readonly case, rename unused test cases --- test-d/union-member.ts | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/test-d/union-member.ts b/test-d/union-member.ts index 39a34d968..366eb5750 100644 --- a/test-d/union-member.ts +++ b/test-d/union-member.ts @@ -1,10 +1,10 @@ import {expectType} from 'tsd'; -import type {UnionMember, IsNever} from '../index.d.ts'; +import type {UnionMember, IsNever, IsEqual} from '../index.d.ts'; -// `UnionMember` distinguishes between different modifiers. -type UnionType = {a: 0} | {b: 0} | {a?: 0} | {readonly a?: 0} | {readonly a: 0}; -expectType({} as UnionMember extends UnionType ? true : false); -expectType({} as UnionType extends UnionMember ? true : false); +type UnionType = {a: 0} | {readonly a: 0}; +type PickedUnionMember = UnionMember; +expectType({} as PickedUnionMember extends UnionType ? true : false); +expectType({} as IsEqual); // `never` acts as a termination condition with `IsNever`. expectType({} as UnionMember); @@ -12,9 +12,14 @@ expectType({} as UnionMember); expectType({} as UnionMember); expectType({} as UnionMember); -// Ensure exactly one member is selected at a time, while covering all members in the union. -type UnionToTupleWithExclude> = +// `WrapMemberInTuple` ensures `UnionMember` selects exactly one member at a time. +type WrapMemberInTuple> = IsNever extends false - ? UnionToTupleWithExclude> | [L] + ? WrapMemberInTuple> | [L] : never; -expectType<[1] | [2] | [3]>({} as UnionToTupleWithExclude<1 | 2 | 3>); +expectType<[1] | [2] | [3]>({} as WrapMemberInTuple<1 | 2 | 3>); +expectType<['foo'] | ['bar'] | ['baz']>({} as WrapMemberInTuple<'foo' | 'bar' | 'baz'>); +expectType<[1] | ['foo'] | [true] | [100n] | [null] | [undefined]>( + {} as WrapMemberInTuple<1 | 'foo' | true | 100n | null | undefined>, +); +expectType<[{a: string}] | [{b: number}]>({} as WrapMemberInTuple<{a: string} | {b: number}>); From 777dc54186bd25c04e336d6e2324312c389790ea Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Tue, 3 Mar 2026 10:35:53 +0900 Subject: [PATCH 13/18] test: remove unused test --- test-d/union-member.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test-d/union-member.ts b/test-d/union-member.ts index 366eb5750..620525a4b 100644 --- a/test-d/union-member.ts +++ b/test-d/union-member.ts @@ -3,7 +3,6 @@ import type {UnionMember, IsNever, IsEqual} from '../index.d.ts'; type UnionType = {a: 0} | {readonly a: 0}; type PickedUnionMember = UnionMember; -expectType({} as PickedUnionMember extends UnionType ? true : false); expectType({} as IsEqual); // `never` acts as a termination condition with `IsNever`. From fe03131d8c4fe3b93ad9a0b03386286d129805d1 Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Tue, 3 Mar 2026 11:05:47 +0900 Subject: [PATCH 14/18] doc: rewrite `UnionMember` example Co-authored-by: Som Shekhar Mukherjee <49264891+som-sm@users.noreply.github.com> --- source/union-member.d.ts | 46 +++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/source/union-member.d.ts b/source/union-member.d.ts index e103f6cdb..519ac5f25 100644 --- a/source/union-member.d.ts +++ b/source/union-member.d.ts @@ -4,27 +4,53 @@ import type {IsNever} from './is-never.d.ts'; /** Returns an arbitrary member of a union type. -Returns `never` when the input is `never`. +Use-cases: +- Implementing recursive type functions that accept a union type. @example ``` -import type {UnionMember} from 'type-fest'; +import type {UnionMember, IsNever} from 'type-fest'; -type Last = UnionMember<1 | 2 | 3>; +type UnionLength = + UnionMember extends infer Member + ? IsNever extends false + ? UnionLength, [...Acc, Member]> + : Acc['length'] + : never; + +type T1 = UnionLength<'foo' | 'bar' | 'baz'>; //=> 3 -type LastNever = UnionMember; -//=> never +type T2 = UnionLength<{a: string}>; +//=> 1 ``` +- Picking an arbitrary member from a union + @example ``` -import type {UnionMember, IsNever} from 'type-fest'; +import type {UnionMember, Primitive, LiteralToPrimitive} from 'type-fest'; + +type IsHomogenous = [T] extends [LiteralToPrimitive>] ? true : false; + +type T1 = IsHomogenous<1 | 2 | 3 | 4>; +//=> true + +type T2 = IsHomogenous<'foo' | 'bar'>; +//=> true -type UnionToTuple> = - IsNever extends false - ? [...UnionToTuple>, L] - : []; +type T3 = IsHomogenous<'foo' | 'bar' | 1>; +//=> false +``` + +Returns `never` when the input is `never`. + +@example +``` +import type {UnionMember} from 'type-fest'; + +type LastNever = UnionMember; +//=> never ``` @category Type From 12cd358b61601bd5451e2c11aa344f148df22933 Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee Date: Tue, 3 Mar 2026 11:36:00 +0530 Subject: [PATCH 15/18] test: cleanup --- test-d/union-member.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/test-d/union-member.ts b/test-d/union-member.ts index 620525a4b..cf309dba1 100644 --- a/test-d/union-member.ts +++ b/test-d/union-member.ts @@ -5,9 +5,7 @@ type UnionType = {a: 0} | {readonly a: 0}; type PickedUnionMember = UnionMember; expectType({} as IsEqual); -// `never` acts as a termination condition with `IsNever`. expectType({} as UnionMember); - expectType({} as UnionMember); expectType({} as UnionMember); From 0c12aef5fc34acbd35c5b76943f31d144dd3cdd5 Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee Date: Tue, 3 Mar 2026 11:39:54 +0530 Subject: [PATCH 16/18] test: add more cases --- test-d/union-member.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test-d/union-member.ts b/test-d/union-member.ts index cf309dba1..dfc634eda 100644 --- a/test-d/union-member.ts +++ b/test-d/union-member.ts @@ -5,6 +5,18 @@ type UnionType = {a: 0} | {readonly a: 0}; type PickedUnionMember = UnionMember; expectType({} as IsEqual); +expectType({} as boolean extends UnionMember ? true : false); +expectType({} as (1 | 'foo' | 'bar') extends UnionMember<1 | 'foo' | 'bar'> ? true : false); +expectType({} as ({foo: string} | {bar: number}) extends UnionMember<{foo: string} | {bar: number}> ? true : false); + +expectType({} as UnionMember); +expectType({} as UnionMember); +expectType({} as UnionMember); +expectType({} as UnionMember); +expectType({} as UnionMember); +expectType({} as any as UnionMember); +expectType({} as any as UnionMember); + expectType({} as UnionMember); expectType({} as UnionMember); expectType({} as UnionMember); From 7295008f7ea38eb57b52b4f0d48d740542aa268e Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee Date: Tue, 3 Mar 2026 11:50:47 +0530 Subject: [PATCH 17/18] test: add comment for `{a: 0} | {readonly a: 0}` case --- test-d/union-member.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test-d/union-member.ts b/test-d/union-member.ts index dfc634eda..171feaf9d 100644 --- a/test-d/union-member.ts +++ b/test-d/union-member.ts @@ -1,10 +1,6 @@ import {expectType} from 'tsd'; import type {UnionMember, IsNever, IsEqual} from '../index.d.ts'; -type UnionType = {a: 0} | {readonly a: 0}; -type PickedUnionMember = UnionMember; -expectType({} as IsEqual); - expectType({} as boolean extends UnionMember ? true : false); expectType({} as (1 | 'foo' | 'bar') extends UnionMember<1 | 'foo' | 'bar'> ? true : false); expectType({} as ({foo: string} | {bar: number}) extends UnionMember<{foo: string} | {bar: number}> ? true : false); @@ -32,3 +28,9 @@ expectType<[1] | ['foo'] | [true] | [100n] | [null] | [undefined]>( {} as WrapMemberInTuple<1 | 'foo' | true | 100n | null | undefined>, ); expectType<[{a: string}] | [{b: number}]>({} as WrapMemberInTuple<{a: string} | {b: number}>); + +type UnionType = {a: 0} | {readonly a: 0}; +type PickedUnionMember = UnionMember; +// We can't use `UnionType extends PickedUnionMember ? true : false` for testing here, +// because that would always be `true` as `UnionType` extends both of its members individually. +expectType({} as IsEqual); From eb1e0b7d6a7e59e39317b63e5eae484841c5956c Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee Date: Tue, 3 Mar 2026 11:52:59 +0530 Subject: [PATCH 18/18] fix: README --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index b9b2ac046..75c0a6f37 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`. -- [`UnionMember`](source/union-member.d.ts) - Returns a member of a union type. Order is not guaranteed. +- [`UnionMember`](source/union-member.d.ts) - Returns an arbitrary member of a union type. ### Type Guard