7

Consider the following:

type Properties = {
    foo: { n: number };
    bar: { s: string };
    baz: { b: boolean };
};

declare function retrieveValues<K extends keyof Properties>(add?: K[]): Pick<Properties, K>[K];

// what happens
const x: { n: number } | { s: string } = retrieveValues(['foo', 'bar']);

// what I'm really trying to express (type error)
const y: { n: number } & { s: string } = retrieveValues(['foo', 'bar']);

Is there a way to get an intersection of the properties of Pick<Properties, K>? Or just a different way to get the intersection of a set of types based on the presence of relevant strings in an array?

Carl Patenaude Poulin
  • 6,238
  • 5
  • 24
  • 46

3 Answers3

14

Using conditional types and type inference in conditional types it is possible to transform { n: number } | { s: string } directly into { n: number } & { s: string }.

type GetKeys<U> = U extends Record<infer K, any> ? K : never

type UnionToIntersection<U extends object> = {
   [K in GetKeys<U>]: U extends Record<K, infer T> ? T : never
}

type Transformed = UnionToIntersection<{ a: string } | { b: number }>
// Transformed has type {a: string, b: number}

Playground Link

The reason this works is basically because conditional types are distributed over union types. From the conditional types pull request:

Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiation. For example, an instantiation of T extends U ? X : Y with the type argument A | B | C for T is resolved as (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y).

KPD
  • 5,281
  • 4
  • 20
  • 19
  • 1
    Recent TS type features are designed in a way they can be used as primitives to build a wide array of type transformations (instead of "hard-coding" those transformations into the language) – wkrueger Jun 19 '18 at 16:15
  • 2
    Great example of conditional types and infer operator. This should be the top answer for TS >= 2.8. – Ravenscar Jul 02 '18 at 23:14
  • 2
    @KPD Can you explain to me why if I change `[K in GetKeys]` into `[K in keyof U]` it does not work the same way anymore? I guess it is because in the instantiation of the `GetKeys` there is a conditional type expression, so it distributes into `UnionToIntersection` somehow, but I cannot figure out exactly. Thank you in advance. – Daniel Nov 30 '18 at 01:09
  • 3
    @Daniel Because the plain `keyof` operator will only include keys that exist in every branch of the union. The `GetKeys` type in this answer gets keys that exist in any branch of the union. A quick example: `keyof ({a: any} | {b: any})` is `never` (because there are no keys that exist in both branches of the union), but `GetKeys<{a: any} | {b: any}>` is `'a' | 'b'`. – KPD Feb 20 '20 at 07:17
  • Great answer! Wish they would have a less obscure/more direct way to achieve this. – gp-v Dec 09 '21 at 22:27
4

UPDATE: this answer was originally written before conditional types were introduced into the language. For newer versions of TypeScript, you can indeed transform arbitrary unions into intersections:

type UnionToIntersection<U> =
    (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never

declare function retrieveValues<K extends keyof Properties>(
    add?: K[]): UnionToIntersection<Properties[K]>;

const x = retrieveValues(['foo', 'bar']);
/* const x: {
    n: number;
} & {
    s: string;
} */

Or you could write something specifically to merge the properties that are picked from a type:

type PickMerge<T, K extends keyof T> =
    { [P in K]: { [Q in keyof T[P]]: [Q, T[P][Q]] }[keyof T[P]] }[K] extends infer U ?
    [U] extends [[PropertyKey, any]] ? { [KV in U as KV[0]]: KV[1] } : never : never

declare function retrieveValues<K extends keyof Properties>(
    add?: K[]): PickMerge<Properties, K>;

const x = retrieveValues(['foo', 'bar']);
/* const x: {
    n: number;
    s: string;
} */

More explanation upon request.

Playground link to code


TS 2.7- answer:

There's no straightforward type operator which, say, turns a union into an intersection, or allows you to iterate union types and do stuff programmatically with the pieces. So on the face of it you're stuck.

Backing up, if you allow yourself to build Properties from pieces instead of trying to break the pieces apart, you can do this:

type InnerProperties = {
  n: number;
  s: string;
  b: boolean;
}

type OuterProperties = {
  foo: "n";
  bar: "s";
  baz: "b";
}

You can see how each key in OuterProperties is a mapping to a key in InnerProperties. (Note that in your Properties, each outer property had a single inner property. You aren't restricted to that, though. If you wanted, say, the "foo" outer key to correspond to something with multiple inner properties like {n: number, r: RegExp} then you would add r: RegExp to InnerProperties and put foo: "n"|"r" in OuterProperties.)

Now you can pick out partial properties like this:

type PickProps<P extends keyof OuterProperties = keyof OuterProperties> = {
  [K in OuterProperties[P]]: InnerProperties[K];
}

So PickProps<"foo"> is {n: number}, and PickProps<"bar"> is {s: string}, and PickProps<"baz"> is {b: boolean}. And notice that PickProps<"foo"|"bar"> is {n: number; s: string}, so we have the output type of retrieveValues() ready. We still have to define Properties in terms of InnerProperties and OuterProperties, like this:

type Properties = {
  [K in keyof OuterProperties]: PickProps<K>
}

And finally you can declare that function the way you want it:

declare function retrieveValues<K extends keyof Properties>(add?: K[]): PickProps<K>;
const y: { n: number } & { s: string } = retrieveValues(['foo', 'bar']);

So that works. Hope that's helpful. Good luck!

jcalz
  • 264,269
  • 27
  • 359
  • 360
0

To get the full set of all possible keys and values in a union, use

type Intersect<T> =
    (T extends any ? (x: T) => any : never) extends
    (x: infer R) => any ? R : never
type ValueIntersectionByKeyUnion<T, TKey extends keyof Intersect<T> = keyof Intersect<T>> = T extends Record<TKey, any> ? ({
    [P in TKey]: T extends Record<P, any> ? (k: T[P]) => void : never
}[TKey] extends ((k: infer I) => void) ? I : never) : never;
type Usage = { [K in keyof Intersect<TA1>]: ValueIntersectionByKeyUnion<TA1, K> };
Arlen Beiler
  • 15,336
  • 34
  • 92
  • 135