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

asn*_*aeb 4 typescript typescript-generics

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

我知道可以选择一个单一的房产

<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]
Run Code Online (Sandbox Code Playgroud)

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

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

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.
Run Code Online (Sandbox Code Playgroud)

jca*_*alz 5

首先,让我们创建一个名为 的辅助类型,PickOnly<T, K>您可以在其中获取类对象类型T和键类型K(或此类键的并集),并生成一个新的类对象类型,其中已知存在Twith 键的属性(就像实用程序类型) 中的键已知存在( 中 中不需要):KPick<T, K>TPick<T, K>

type PickOnly<T, K extends keyof T> =
    Pick<T, K> & { [P in Exclude<keyof T, K>]?: never };
Run Code Online (Sandbox Code Playgroud)

该实现与禁止除 中的键以外的键的类型相交 。该类型使用实用程序类型获取非键,并表示它们必须都是可选属性,其值类型是不可能类型。可选属性可能会丢失(或取决于编译器选项),但属性不能存在......这意味着这些属性必须始终丢失(或)。Pick<T, K>TK{[P in Exclude<keyof T, K>]?: never}Exclude<T, U>KTneverundefinedneverundefined

一个例子:

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
Run Code Online (Sandbox Code Playgroud)

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


因此,您想要的可能是每个可能的键集(其中最多有两个元素)组成AtMostTwoKeys<T>的并集。因为看起来像PickOnly<T, K>KTArticle

| 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
Run Code Online (Sandbox Code Playgroud)

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

type AtMostTwoKeys<T> = (
    PickOnly<T, never> |    
)'
Run Code Online (Sandbox Code Playgroud)

现在对于一个关键...最简单的方法是通过microsoft/TypeScript#47109中创造的形式的分布式对象类型。形式的类型,您可以立即索引到映射类型,并生成for all in the union的并集。{[K in KK]: F<K>}[KK]F<K>KKK

因此对于一把钥匙来说,看起来像:

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

哦,但是in keyof T使映射类型同态,这可能会undefined在可选输入属性的输出中引入不需要的值,我将抢先使用-?映射类型修饰符从映射中删除可选修饰符:

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

对于两个键来说,事情有点棘手。我们想要在这里做两层分布式对象。第一个迭代K中的每个键keyof T,第二个应该引入一个新的类型参数(例如L)来执行相同的操作。然后K | L将是来自 的每对可能的键keyof T,以及每个单个键(当KL相同时)。这会重复计算不同的对,但这不会造成任何伤害:

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]
) 
Run Code Online (Sandbox Code Playgroud)

基本上就是这样,但结果类型将用以下形式表示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"> */
Run Code Online (Sandbox Code Playgroud)

也许这样就可以了。但通常我喜欢引入一个小助手来将这些类型扩展到它们的实际属性:

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
Run Code Online (Sandbox Code Playgroud)

让我们再试一次:

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
*/
Run Code Online (Sandbox Code Playgroud)

看起来不错!


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

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!
Run Code Online (Sandbox Code Playgroud)

成功!

Playground 代码链接