From bed43c1e26d9f96b949e0747d7dbabe8c643cefc Mon Sep 17 00:00:00 2001 From: Ifiok Jr Date: Mon, 9 Mar 2020 06:45:32 +0100 Subject: [PATCH] Add `UnionToIntersection` type (#89) Co-authored-by: Sindre Sorhus --- index.d.ts | 1 + readme.md | 1 + source/set-optional.d.ts | 4 ++- source/set-required.d.ts | 4 ++- source/union-to-intersection.d.ts | 58 +++++++++++++++++++++++++++++++ test-d/union-to-intersection.ts | 9 +++++ 6 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 source/union-to-intersection.d.ts create mode 100644 test-d/union-to-intersection.ts 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 a1ead49a3..82f00bd67 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) - Convert a union type to an intersection type. ### Miscellaneous diff --git a/source/set-optional.d.ts b/source/set-optional.d.ts index a9a256aac..35398992b 100644 --- a/source/set-optional.d.ts +++ b/source/set-optional.d.ts @@ -1,3 +1,5 @@ +import {Except} from './except'; + /** Create a type that makes the given keys optional. The remaining keys are kept as is. The sister of the `SetRequired` type. @@ -23,7 +25,7 @@ type SomeOptional = SetOptional; */ export type SetOptional = // Pick just the keys that are not optional from the base type. - Pick> & + Except & // Pick the keys that should be optional from the base type and make them optional. Partial> extends // If `InferredType` extends the previous, then for each key, use the inferred type key. diff --git a/source/set-required.d.ts b/source/set-required.d.ts index 2572bc12c..0a7233077 100644 --- a/source/set-required.d.ts +++ b/source/set-required.d.ts @@ -1,3 +1,5 @@ +import {Except} from './except'; + /** Create a type that makes the given keys required. The remaining keys are kept as is. The sister of the `SetOptional` type. @@ -23,7 +25,7 @@ type SomeRequired = SetRequired; */ export type SetRequired = // Pick just the keys that are not required from the base type. - Pick> & + Except & // Pick the keys that should be required from the base type and make them required. Required> extends // If `InferredType` extends the previous, then for each key, use the inferred type key. diff --git a/source/union-to-intersection.d.ts b/source/union-to-intersection.d.ts new file mode 100644 index 000000000..61d98412e --- /dev/null +++ b/source/union-to-intersection.d.ts @@ -0,0 +1,58 @@ +/** +Convert 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 [this Stack Overflow answer](https://stackoverflow.com/a/50375286/2172153). + +@example +``` +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 its way into your library code follows. + +@example +``` +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 unknown + // 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);