在的帮助下Exclude
type 是在 TypeScript 2.8 中添加的,提供了一种需要至少一组属性中的一个的通用方法:
type RequireAtLeastOne<T, Keys extends keyof T = keyof T> =
Pick<T, Exclude<keyof T, Keys>>
& {
[K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>
}[Keys]
要求提供一个且仅一个的部分但不是绝对的方法是:
type RequireOnlyOne<T, Keys extends keyof T = keyof T> =
Pick<T, Exclude<keyof T, Keys>>
& {
[K in Keys]-?:
Required<Pick<T, K>>
& Partial<Record<Exclude<Keys, K>, undefined>>
}[Keys]
这是一个 TypeScript Playground 链接,显示了两者的实际效果。 https://www.typescriptlang.org/play/#src=type%20RequireAtLeastOne%3CT%2C%20Keys%20extends%20keyof%20T%20%3D%20keyof%20T%3E%20%3D%0D%0A%20%20%20%20Pick%3CT%2C%20Exclude%3Ckeyof%20T%2C%20Keys%3E%3E%0D%0A%20%20%20%20%26%20%7B%0D%0A%20%20%20%20%20%20%20%20%5BK%20in%20Keys%5D-%3F%3A%20Required%3CPick%3CT%2C%20K%3E%3E%20%26%20Partial%3CPick%3CT%2C%20Exclude%3CKeys%2C%20K%3E%3E%3E%0D%0A%20%20%20%20%7D%5BKeys%5D%0D%0A%0D%0Ainterface%20MenuItem%20%7B%0D%0A%20%20title%3A%20string%3B%0D%0A%20%20component%3F%3A%20number%3B%0D%0A%20%20click%3F%3A%20number%3B%0D%0A%20%20icon%3A%20string%3B%0D%0A%7D%0D%0A%0D%0Atype%20ClickOrComponent%20%3D%20RequireAtLeastOne%3CMenuItem%2C%20'click'%20%7C%20'component'%3E%0D%0A%0D%0Aconst%20withComponent%3A%20ClickOrComponent%20%3D%20%7B%0D%0A%20%20%20%20title%3A%20%22test%22%2C%0D%0A%20%20%20%20component%3A%2052%2C%0D%0A%20%20%20%20icon%3A%20%22icon%22%0D%0A%7D%0D%0A%0D%0Aconst%20withClick%3A%20ClickOrComponent%20%3D%20%7B%0D%0A%20%20%20%20title%3A%20%22test%22%2C%0D%0A%20%20%20%20click%3A%2054%2C%0D%0A%20%20%20%20icon%3A%20%22icon%22%0D%0A%7D%0D%0A%0D%0Aconst%20errorWithNeither%3A%20ClickOrComponent%20%3D%20%7B%0D%0A%20%20%20%20title%3A%20%22test%22%2C%0D%0A%20%20%20%20icon%3A%20%22icon%22%0D%0A%7D%0D%0A%0D%0Aconst%20noErrorWithBoth%3A%20ClickOrComponent%20%3D%20%7B%0D%0A%20%20%20%20title%3A%20%22test%22%2C%0D%0A%20%20%20%20click%3A%2054%2C%0D%0A%20%20%20%20component%3A%2024%2C%0D%0A%20%20%20%20icon%3A%20%22icon%22%0D%0A%7D%0D%0A%0D%0Aconst%20errorWithBothWhenOneHasWrongType%3A%20ClickOrComponent%20%3D%20%7B%0D%0A%20%20%20%20title%3A%20%22test%22%2C%0D%0A%20%20%20%20click%3A%2054%2C%0D%0A%20%20%20%20component%3A%20%22should%20be%20number%20here%22%2C%0D%0A%20%20%20%20icon%3A%20%22icon%22%0D%0A%7D%0D%0A%0D%0A%0D%0Atype%20RequireOnlyOne%3CT%2C%20Keys%20extends%20keyof%20T%20%3D%20keyof%20T%3E%20%3D%0D%0A%20%20%20%20Pick%3CT%2C%20Exclude%3Ckeyof%20T%2C%20Keys%3E%3E%0D%0A%20%20%20%20%26%20%7B%20%5BK%20in%20Keys%5D-%3F%3A%0D%0A%20%20%20%20%20%20%20%20Required%3CPick%3CT%2C%20K%3E%3E%0D%0A%20%20%20%20%20%20%20%20%26%20Partial%3CRecord%3CExclude%3CKeys%2C%20K%3E%2C%20undefined%3E%3E%0D%0A%20%20%20%20%7D%5BKeys%5D%0D%0A%0D%0Atype%20OnlyOneClickOrComponent%20%3D%20RequireOnlyOne%3CMenuItem%2C%20'click'%20%7C%20'component'%3E%0D%0A%0D%0Aconst%20noErrorWithOnlyOne%3A%20OnlyOneClickOrComponent%20%3D%20%7B%0D%0A%20%20%20%20title%3A%20%22test%22%2C%0D%0A%20%20%20%20click%3A%20534%2C%0D%0A%20%20%20%20icon%3A%20%22icon%22%0D%0A%7D%0D%0A%0D%0Aconst%20errorWithBoth%3A%20OnlyOneClickOrComponent%20%3D%20%7B%0D%0A%20%20%20%20title%3A%20%22test%22%2C%0D%0A%20%20%20%20click%3A%20534%2C%0D%0A%20%20%20%20component%3A%2053%2C%0D%0A%20%20%20%20icon%3A%20%22icon%22%0D%0A%7D%0D%0A%0D%0A%0D%0A%2F%2F%20This%20interface%20will%20be%20used%20to%20trick%20OnlyOneClickAtComponent%20into%20allowing%20an%20object%20with%20both%20%20%0D%0Ainterface%20ClickMenuItem%20%7B%0D%0A%20%20title%3A%20string%3B%0D%0A%20%20click%3A%20number%3B%0D%0A%20%20icon%3A%20string%3B%0D%0A%7D%0D%0A%0D%0Aconst%20hasBoth%20%3D%20%7B%0D%0A%20%20%20%20title%3A%20%22test%22%2C%0D%0A%20%20%20%20click%3A%2054%2C%0D%0A%20%20%20%20component%3A%2024%2C%0D%0A%20%20%20%20icon%3A%20%22icon%22%0D%0A%7D%0D%0A%0D%0A%2F%2F%20No%20error%20because%20excess%20properties%20are%20only%20disallowed%20when%20directly%20assigning%20an%20object%20literal%20%0D%0Aconst%20temp%3A%20ClickMenuItem%20%3D%20hasBoth%0D%0A%0D%0A%2F%2F%20No%20error%20despite%20temp%20actually%20having%20both%20because%20TS%20only%20knows%20that%20temp%20is%20a%20ClickMenuItem%20%0D%0Aconst%20trickIntoAllowingBoth%3A%20OnlyOneClickOrComponent%20%3D%20temp%0D%0A
警告与RequireOnlyOne
问题是 TypeScript 并不总是在编译时知道运行时存在的每个属性。很明显RequireOnlyOne
无法采取任何措施来阻止它不知道的额外属性。我提供了一个例子来说明如何RequireOnlyOne
可能会错过游乐场链接末尾的东西。
使用以下示例快速概述其工作原理:
interface MenuItem {
title: string;
component?: number;
click?: number;
icon: string;
}
type ClickOrComponent = RequireAtLeastOne<MenuItem, 'click' | 'component'>
Pick<T, Exclude<keyof T, Keys>>
from RequireAtLeastOne
变成{ title: string, icon: string}
,这是未包含在中的键的未更改属性'click' | 'component'
-
{ [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>> }[Keys]
from RequireAtLeastOne
becomes
{
component: Required<{ component?: number }> & { click?: number },
click: Required<{ click?: number }> & { component?: number }
}[Keys]
这变成了
{
component: { component: number, click?: number },
click: { click: number, component?: number }
}['component' | 'click']
最终变成
{component: number, click?: number} | {click: number, component?: number}
-
上述步骤 1 和 2 的交集
{ title: string, icon: string}
&
({component: number, click?: number} | {click: number, component?: number})
简化为
{ title: string, icon: string, component: number, click?: number}
| { title: string, icon: string, click: number, component?: number}