映射类型的交集

Intersection of mapped types

考虑以下几点:

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']);

有没有办法得到Pick<Properties, K>的属性的交集?或者只是根据数组中相关字符串的存在来获取一组类型的交集的不同方法?

更新:这个答案最初是在 conditional types were introduced into the language. For newer versions of TypeScript, you can indeed :

之前写的
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;
} */

或者您可以专门编写一些内容来合并从类型中选取的属性:

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;
} */

根据要求提供更多解释。

Playground link to code


TS 2.7- 答案:

没有直接的类型运算符可以将并集转换为交集,或者允许您 iterate union types 并以编程方式处理片段。所以从表面上看,你被卡住了。

备份,如果你允许自己从碎片中构建 Properties 而不是试图将碎片分开,你可以这样做:

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

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

您可以看到 OuterProperties 中的每个键如何映射到 InnerProperties 中的键。 (请注意,在您的 Properties 中,每个外部 属性 都有一个内部 属性。不过,您并不局限于此。如果您想要,比如说 "foo"外键对应于具有多个内部属性的东西,比如 {n: number, r: RegExp} 那么你可以将 r: RegExp 添加到 InnerProperties 并将 foo: "n"|"r" 放入 OuterProperties.)

现在您可以像这样挑选部分属性:

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

所以PickProps<"foo">{n: number}PickProps<"bar">{s: string}PickProps<"baz">{b: boolean}。并注意 PickProps<"foo"|"bar">{n: number; s: string},所以我们已经准备好 retrieveValues() 的输出类型。我们仍然需要根据 InnerPropertiesOuterProperties 来定义 Properties,像这样:

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

最后,您可以按照自己的方式声明该函数:

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

这样行得通。希望这会有所帮助。祝你好运!

使用conditional types and type inference in conditional types可以将{ n: number } | { s: string }直接转换为{ 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

这样做的原因基本上是因为条件类型分布在联合类型上。来自 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).