Skip to content

Commit 878b6df

Browse files
taiyakihitotsusindresorhussom-sm
authored
Add UnionMember type (#1368)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com> Co-authored-by: Som Shekhar Mukherjee <iamssmkhrj@gmail.com>
1 parent bbce298 commit 878b6df

File tree

5 files changed

+107
-16
lines changed

5 files changed

+107
-16
lines changed

index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ export type {IsNullable} from './source/is-nullable.d.ts';
170170
export type {TupleOf} from './source/tuple-of.d.ts';
171171
export type {ExclusifyUnion} from './source/exclusify-union.d.ts';
172172
export type {ArrayReverse} from './source/array-reverse.d.ts';
173+
export type {UnionMember} from './source/union-member.d.ts';
173174

174175
// Template literal types
175176
export type {CamelCase, CamelCaseOptions} from './source/camel-case.d.ts';

readme.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ Click the type names for complete docs.
193193
- [`ConditionalSimplifyDeep`](source/conditional-simplify-deep.d.ts) - Recursively simplifies a type while including and/or excluding certain types from being simplified.
194194
- [`ExclusifyUnion`](source/exclusify-union.d.ts) - Ensure mutual exclusivity in object unions by adding other members’ keys as `?: never`.
195195
- [`Optional`](source/optional.d.ts) - Create a type that represents either the value or `undefined`, while stripping `null` from the type.
196+
- [`UnionMember`](source/union-member.d.ts) - Returns an arbitrary member of a union type.
196197

197198
### Type Guard
198199

@@ -359,6 +360,8 @@ Click the type names for complete docs.
359360
- `Maybe`, `Option` - See [`Optional`](source/optional.d.ts)
360361
- `MaybePromise` - See [`Promisable`](source/promisable.d.ts)
361362
- `ReadonlyTuple` - See [`TupleOf`](source/tuple-of.d.ts)
363+
- `LastOfUnion` - See [`UnionMember`](source/union-member.d.ts)
364+
- `FirstOfUnion` - See [`UnionMember`](source/union-member.d.ts)
362365

363366
## Tips
364367

source/union-member.d.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import type {UnionToIntersection} from './union-to-intersection.d.ts';
2+
import type {IsNever} from './is-never.d.ts';
3+
4+
/**
5+
Returns an arbitrary member of a union type.
6+
7+
Use-cases:
8+
- Implementing recursive type functions that accept a union type.
9+
10+
@example
11+
```
12+
import type {UnionMember, IsNever} from 'type-fest';
13+
14+
type UnionLength<T, Acc extends any[] = []> =
15+
UnionMember<T> extends infer Member
16+
? IsNever<Member> extends false
17+
? UnionLength<Exclude<T, Member>, [...Acc, Member]>
18+
: Acc['length']
19+
: never;
20+
21+
type T1 = UnionLength<'foo' | 'bar' | 'baz'>;
22+
//=> 3
23+
24+
type T2 = UnionLength<{a: string}>;
25+
//=> 1
26+
```
27+
28+
- Picking an arbitrary member from a union
29+
30+
@example
31+
```
32+
import type {UnionMember, Primitive, LiteralToPrimitive} from 'type-fest';
33+
34+
type IsHomogenous<T extends Primitive> = [T] extends [LiteralToPrimitive<UnionMember<T>>] ? true : false;
35+
36+
type T1 = IsHomogenous<1 | 2 | 3 | 4>;
37+
//=> true
38+
39+
type T2 = IsHomogenous<'foo' | 'bar'>;
40+
//=> true
41+
42+
type T3 = IsHomogenous<'foo' | 'bar' | 1>;
43+
//=> false
44+
```
45+
46+
Returns `never` when the input is `never`.
47+
48+
@example
49+
```
50+
import type {UnionMember} from 'type-fest';
51+
52+
type LastNever = UnionMember<never>;
53+
//=> never
54+
```
55+
56+
@category Type
57+
*/
58+
export type UnionMember<T> =
59+
IsNever<T> extends true
60+
? never
61+
: UnionToIntersection<T extends any ? () => T : never> extends () => (infer R)
62+
? R
63+
: never;
64+
65+
export {};

source/union-to-tuple.d.ts

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,5 @@
11
import type {IsNever} from './is-never.d.ts';
2-
import type {UnionToIntersection} from './union-to-intersection.d.ts';
3-
4-
/**
5-
Returns the last element of a union type.
6-
7-
@example
8-
```
9-
type Last = LastOfUnion<1 | 2 | 3>;
10-
//=> 3
11-
```
12-
*/
13-
type LastOfUnion<T> =
14-
UnionToIntersection<T extends any ? () => T : never> extends () => (infer R)
15-
? R
16-
: never;
2+
import type {UnionMember} from './union-member.d.ts';
173

184
/**
195
Convert a union type into an unordered tuple type of its elements.
@@ -50,7 +36,7 @@ const petList = Object.keys(pets) as UnionToTuple<Pet>;
5036
5137
@category Array
5238
*/
53-
export type UnionToTuple<T, L = LastOfUnion<T>> =
39+
export type UnionToTuple<T, L = UnionMember<T>> =
5440
IsNever<T> extends false
5541
? [...UnionToTuple<Exclude<T, L>>, L]
5642
: [];

test-d/union-member.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import {expectType} from 'tsd';
2+
import type {UnionMember, IsNever, IsEqual} from '../index.d.ts';
3+
4+
expectType<false>({} as boolean extends UnionMember<boolean> ? true : false);
5+
expectType<false>({} as (1 | 'foo' | 'bar') extends UnionMember<1 | 'foo' | 'bar'> ? true : false);
6+
expectType<false>({} as ({foo: string} | {bar: number}) extends UnionMember<{foo: string} | {bar: number}> ? true : false);
7+
8+
expectType<number>({} as UnionMember<number>);
9+
expectType<string>({} as UnionMember<string>);
10+
expectType<bigint>({} as UnionMember<bigint>);
11+
expectType<true>({} as UnionMember<true>);
12+
expectType<false>({} as UnionMember<false>);
13+
expectType<null>({} as any as UnionMember<null>);
14+
expectType<undefined>({} as any as UnionMember<undefined>);
15+
16+
expectType<never>({} as UnionMember<never>);
17+
expectType<unknown>({} as UnionMember<unknown>);
18+
expectType<any>({} as UnionMember<any>);
19+
20+
// `WrapMemberInTuple` ensures `UnionMember` selects exactly one member at a time.
21+
type WrapMemberInTuple<T, L = UnionMember<T>> =
22+
IsNever<T> extends false
23+
? WrapMemberInTuple<Exclude<T, L>> | [L]
24+
: never;
25+
expectType<[1] | [2] | [3]>({} as WrapMemberInTuple<1 | 2 | 3>);
26+
expectType<['foo'] | ['bar'] | ['baz']>({} as WrapMemberInTuple<'foo' | 'bar' | 'baz'>);
27+
expectType<[1] | ['foo'] | [true] | [100n] | [null] | [undefined]>(
28+
{} as WrapMemberInTuple<1 | 'foo' | true | 100n | null | undefined>,
29+
);
30+
expectType<[{a: string}] | [{b: number}]>({} as WrapMemberInTuple<{a: string} | {b: number}>);
31+
32+
type UnionType = {a: 0} | {readonly a: 0};
33+
type PickedUnionMember = UnionMember<UnionType>;
34+
// We can't use `UnionType extends PickedUnionMember ? true : false` for testing here,
35+
// because that would always be `true` as `UnionType` extends both of its members individually.
36+
expectType<false>({} as IsEqual<UnionType, PickedUnionMember>);

0 commit comments

Comments
 (0)