diff --git a/index.d.ts b/index.d.ts index e2ac6c3ff..fc0e80fa8 100644 --- a/index.d.ts +++ b/index.d.ts @@ -20,6 +20,7 @@ export {AsyncReturnType} from './source/async-return-type'; export {ConditionalExcept} from './source/conditional-except'; export {ConditionalKeys} from './source/conditional-keys'; export {ConditionalPick} from './source/conditional-pick'; +export {UnionToIntersection} from './source/union-to-intersection'; // Miscellaneous export {PackageJson} from './source/package-json'; diff --git a/readme.md b/readme.md index 86c5ec311..d2af40eef 100644 --- a/readme.md +++ b/readme.md @@ -79,6 +79,7 @@ Click the type names for complete docs. - [`ConditionalKeys`](source/conditional-keys.d.ts) - Extract keys from a shape where values extend the given `Condition` type. - [`ConditionalPick`](source/conditional-pick.d.ts) - Like `Pick` except it selects properties from a shape where the values extend the given `Condition` type. - [`ConditionalExcept`](source/conditional-except.d.ts) - Like `Omit` except it removes properties from a shape where the values extend the given `Condition` type. +- [`UnionToIntersection`](source/union-to-intersection.d.ts) - Converts a union type to an intersection type. ### Miscellaneous diff --git a/source/union-to-intersection.d.ts b/source/union-to-intersection.d.ts new file mode 100644 index 000000000..6cdf29c60 --- /dev/null +++ b/source/union-to-intersection.d.ts @@ -0,0 +1,59 @@ +/** +A helpful utility which maps a union type to an intersection type using [distributive conditional types](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#distributive-conditional-types). + +Inspired by a [stack overflow answer](https://stackoverflow.com/a/50375286/2172153). + +@example + +```ts +import {UnionToIntersection} from 'type-fest'; + +type Union = {the(): void} | {great(arg: string): void} | {escape: boolean}; +type Intersection = UnionToIntersection; +//=> {the(): void; great(arg: string): void; escape: boolean}; +``` + +A more applicable example which could make it's way into your library code follows. + +@example + +```ts +import {UnionToIntersection} from 'type-fest'; + +class CommandOne { + commands: { + a1: () => undefined, + b1: () => undefined, + } +} + +class CommandTwo { + commands: { + a2: (argA: string) => undefined, + b2: (argB: string) => undefined, + } +} + +const union = [new CommandOne(), new CommandTwo()].map(instance => instance.commands) +type Union = typeof union; +//=> {a1(): void; b1(): void} | {a2(argA: string): void; b2(argB: string): void} + +type Intersection = UnionToIntersection; +//=> {a1(): void; b1(): void; a2(argA: string): void; b2(argB: string): void} +``` +*/ +export type UnionToIntersection = ( + // `extends any` is always going to be the case and is used to convert the + // `Union` into a [distributive conditional + // type](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#distributive-conditional-types). + Union extends any + // The union type is used as the only argument to a function since the union + // of function arguments is an intersection. + ? (distributedUnion: Union) => void + // This won't happen. + : never + // Infer the `Intersection` type since TypeScript represents the positional + // arguments of unions of functions as an intersection of the union. + ) extends ((mergedIntersection: infer Intersection) => void) + ? Intersection + : never; diff --git a/test-d/union-to-intersection.ts b/test-d/union-to-intersection.ts new file mode 100644 index 000000000..5f63226ad --- /dev/null +++ b/test-d/union-to-intersection.ts @@ -0,0 +1,9 @@ +import {expectType} from 'tsd'; +import {UnionToIntersection} from '..'; + +declare const intersection1: UnionToIntersection<{a: string} | {b: number}>; +expectType<{a: string; b: number}>(intersection1); + +// Creates a union of matching properties. +declare const intersection2: UnionToIntersection<{a: string} | {b: number} | {a: () => void}>; +expectType<{a: string | (() => void); b: number}>(intersection2);