首先,让我们创建一个名为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>
大概是每K
个PickOnly<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 into和mapped 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
中的每一个可能的密钥对,以及每一个密钥(当K
和L
相同时).这会加倍计算不同的配对,但这不会造成任何伤害:
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