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

我知道,可以 Select 一个带有

<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]

但没有弄清楚如何(以及是否)可以使用这种方法 Select 两个属性.

下面是一个我想如何使用它的例子

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>的帮助器类型,在这里,您获取一个类似于T类型的对象和一个键类型K(或union个这样的键),并生成一个新的类似于对象的类型,其中K中有键的T的属性已知存在(就像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>的类型禁止T中的密钥,而不是K中的密钥.{[P in Exclude<keyof T, K>]?: never}类型使用the Exclude<T, U> utility type来获取T的非K键,并表示它们都必须是optional properties,其值类型为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>大概是每KPickOnly<T, K>的并集,由T中的每一组可能的键组成,其中最多有两个元素.对于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>个.没有 keys 的部分很简单:

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

现在有一把 keys ...最简单的方法是使用microsoft/TypeScript#47109年发明的distributive object type.这是一种表格{[K in KK]: F<K>}[KK]的类型,在表格index intomapped type中,对于KK个并集中的所有K个,立即生成F<K>个并集.

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

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

但是in keyof T使映射类型为homomorphic,这可能会在可选输入属性的输出中引入不需要的undefined值,我将先发制人地从映射中删除可选性修饰符use the -? mapped type modifier:

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

对于两把 keys 来说,事情有点棘手.我们想在这里做two层分布对象.第一个循环遍历keyof T中的每个键K,第二个循环应该引入一个新的类型参数(比如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]
) 

基本上就是这样,但最终的类型将以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"> */

也许没关系.但通常我喜欢在expand out such types的实际属性中引入一个小助手:

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

Typescript相关问答推荐

angular 17独立使用多个组件(例如两个组件)

为什么从数组中删除一个值会删除打字错误?

缩小并集子键后重新构造类型

如何避免推断空数组

为什么在leetcode问题的测试用例中,即使我确实得到了比预期更好的解决方案,这段代码也失败了?

布局组件中的Angular 17命名路由出口

防止重复使用 Select 器重新渲染

TypeScrip中的类型安全包装类

下载量怎么会超过下载量?

(TypeScript)TypeError:无法读取未定义的属性(正在读取映射)"

是否可以强制静态方法将同一类型的对象返回其自己的类?

为什么发送空字段?

Angular 16将独立组件作为对话框加载,而不进行布线或预加载

如何在Reaction 18、Reaction-Rout6中的导航栏中获取路由参数

编剧- Select 动态下拉选项

埃斯林特警告危险使用&as";

如何为带有参数的函数类型指定类型保护?

为什么过滤器不改变可能的类型?

通用可选函数参数返回类型

该表达式不可调用,联合体的每个成员