diff --git a/index.d.ts b/index.d.ts index bba3afd97..e39297550 100644 --- a/index.d.ts +++ b/index.d.ts @@ -169,6 +169,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 {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 305a3a546..eb41f1119 100644 --- a/readme.md +++ b/readme.md @@ -192,6 +192,7 @@ Click the type names for complete docs. - [`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`. - [`Optional`](source/optional.d.ts) - Create a type that represents either the value or `undefined`, while stripping `null` from the type. +- [`UnionMember`](source/union-member.d.ts) - Returns an arbitrary member of a union type. ### Type Guard @@ -358,6 +359,8 @@ Click the type names for complete docs. - `Maybe`, `Option` - See [`Optional`](source/optional.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/union-member.d.ts b/source/union-member.d.ts new file mode 100644 index 000000000..519ac5f25 --- /dev/null +++ b/source/union-member.d.ts @@ -0,0 +1,65 @@ +import type {UnionToIntersection} from './union-to-intersection.d.ts'; +import type {IsNever} from './is-never.d.ts'; + +/** +Returns an arbitrary member of a union type. + +Use-cases: +- Implementing recursive type functions that accept a union type. + +@example +``` +import type {UnionMember, IsNever} from 'type-fest'; + +type UnionLength = + UnionMember extends infer Member + ? IsNever extends false + ? UnionLength, [...Acc, Member]> + : Acc['length'] + : never; + +type T1 = UnionLength<'foo' | 'bar' | 'baz'>; +//=> 3 + +type T2 = UnionLength<{a: string}>; +//=> 1 +``` + +- Picking an arbitrary member from a union + +@example +``` +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 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 +*/ +export type UnionMember = + IsNever extends true + ? 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..9ef76be3d 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 {UnionMember} from './union-member.d.ts'; /** Convert a union type into an unordered tuple type of its elements. @@ -50,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/union-member.ts b/test-d/union-member.ts new file mode 100644 index 000000000..171feaf9d --- /dev/null +++ b/test-d/union-member.ts @@ -0,0 +1,36 @@ +import {expectType} from 'tsd'; +import type {UnionMember, IsNever, IsEqual} from '../index.d.ts'; + +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); + +// `WrapMemberInTuple` ensures `UnionMember` selects exactly one member at a time. +type WrapMemberInTuple> = + IsNever extends false + ? WrapMemberInTuple> | [L] + : never; +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}>); + +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);