从 Typescript 中的类型中仅选择两个属性

Pick only two properties from a type in Typescript

我只需要从一个类型中选择两个名称尚未定义的属性,然后从那里创建一个新类型,其中一个属性是必需的,另一个属性是可选的。

我知道可以选择一个 属性 和

<T extends Record<string,any>> {
    [K in keyof T]: (Record<K, T[K]> & 
    Partial<Record<Exclude<keyof T, K>, never>>) extends infer U ? { [P in keyof U]: U[P] } : never
}[keyof T]

但没有弄清楚如何(以及是否)可以使用此方法选择两个属性。

下面是我想如何使用它的示例

class Article {
    name: string
    id: number
    content?: string
}

const article: TwoKeys<Article> = { id: 23 } // no error
const article: TwoKeys<Article> = { name: "my article", id: 122 } // no error
const article: TwoKeys<Article> = { name: "my article" , id: 23, content: "my content" } // error! we passed more than two props.

首先,让我们创建一个名为 PickOnly<T, K> 的辅助类型,其中您采用 object-like 类型 T 和一个键类型 K(或 union of such keys) and produce a new object-like type where the properties of T with keys in K are known to be present (just like the Pick<T, K> utility type) ,并且已知 T 中的键 not 不存在(Pick<T, K> 中不需要):

type PickOnly<T, K extends keyof T> =
    Pick<T, K> & { [P in Exclude<keyof T, K>]?: never };

实施intersects Pick<T, K> with a type that prohibits keys in T other than those in K. The type {[P in Exclude<keyof T, K>]?: never} uses the Exclude<T, U> utility type to get the non-K keys of T, and says that they must all be optional properties whose value type is the impossible never type。可选的 属性 可能会丢失(或 undefined 取决于编译器选项),但 never 属性 不能存在......这意味着这些属性必须始终丢失(或 undefined).

一个例子:

let x: PickOnly<{a: string, b: number, c: boolean}, "a" | "c">;
x = {a: "", c: true} // okay
x = {a: "", b: 123, c: true} // error!
// -------> ~
//Type 'number' is not assignable to type 'never'.
x = {a: ""}; // error! Property 'c' is missing

X 类型的值必须是 {a: number, c: boolean},而且根本不能包含 b 属性。


因此,您想要的 AtMostTwoKeys<T> 大概是 PickOnly<T, K> 的并集,每个 KT 中的每组可能的键组成,最多两个元素。 Article 看起来像

| PickOnly<Article, never> // no keys
| PickOnly<Article, "name"> // only name
| PickOnly<Article, "id"> // only id
| PickOnly<Article, "content"> // only content
| PickOnly<Article, "name" | "id"> // name and id
| PickOnly<Article, "name" | "content"> // name and content
| PickOnly<Article, "id" | "content"> // id and content

那么让我们构建 AtMostTwoKeys<T>。没有按键的部分很简单:

type AtMostTwoKeys<T> = (
    PickOnly<T, never> |    
)'

现在对于一个键...最简单的方法是通过 分布式对象类型 microsoft/TypeScript#47109. A type of the form {[K in KK]: F<K>}[KK], where you immediately index into a mapped type 中创造的形式产生 F<K> KK 联盟中的所有 K

所以对于一个键,它看起来像:

type AtMostTwoKeys<T> = (
    PickOnly<T, never> |
    { [K in keyof T]: PickOnly<T, K> }[keyof T]
);

哦,但是 in keyof T 使映射类型 , which will possibly introduce unwanted undefined values in the output for optional input properties, I will preemptively use the -? mapped type modifier 从映射中删除可选修饰符:

type AtMostTwoKeys<T> = (
    PickOnly<T, never> |
    { [K in keyof T]-?: PickOnly<T, K> }[keyof T]
);

对于两个键,事情有点棘手。我们想在这里做层分布式对象。第一个迭代 keyof T 中的每个键 K,第二个应该引入一个新的类型参数(比如 L)来做同样的事情。然后 K | L 将是来自 keyof T 的每对可能的键,以及每个键(当 KL 相同时)。这 double-counts 个不同的对,但这不会伤害任何东西:

type AtMostTwoKeys<T> = (
    PickOnly<T, never> |
    { [K in keyof T]-?: PickOnly<T, K> |
        { [L in keyof T]-?:
            PickOnly<T, K | L> }[keyof T]
    }[keyof T]
) 

基本上就是这样,但是结果类型将以 PickOnly:

的形式表示
type AMTKA = AtMostTwoKeys<Article>;
/* type AMTKA = PickOnly<Article, never> | PickOnly<Article, "name"> | 
  PickOnly<Article, "name" | "id"> | PickOnly<Article, "name" | "content"> | 
  PickOnly<Article, "id"> | PickOnly<Article, "id" | "content"> | \
  PickOnly<Article, "content"> */

也许这很好。但通常我喜欢在它们的实际属性中引入一个的小帮手:

type AtMostTwoKeys<T> = (
    PickOnly<T, never> |
    { [K in keyof T]-?: PickOnly<T, K> |
        { [L in keyof T]-?:
            PickOnly<T, K | L> }[keyof T]
    }[keyof T]
) extends infer O ? { [P in keyof O]: O[P] } : never

我们再试一次:

type AMTKA = AtMostTwoKeys<Article>;
/* type AMTKA = 
| {  name?: never;  id?: never;  content?: never; } // no keys
| {  name: string;  id?: never;  content?: never; } // only name
| {  name: string;  id: number;  content?: never; } // name and id
| {  name: string;  content?: string;  id?: never; } // name and content
| {  id: number;  name?: never;  content?: never; }  // only id
| {  id: number;  content?: string;  name?: never; } // id and content
| {  content?: string;  name?: never;  id?: never; } // only content
*/

看起来不错!


为了确定,让我们检查一下您的示例用例:

let article: AtMostTwoKeys<Article>;
article = { id: 23 } // okay
article = { name: "my article", id: 122 } // okay
article = { name: "my article", id: 23, content: "my content" } // error!

成功!

Playground link to code