如果我理解正确,你想让
OneKey<"a" | "b">
变成类似于
{a: any, b?: never} | {a?: never, b: any}
的东西。这意味着它
要么有一个
a
键或一个
b
键
但不是同时拥有两个。所以你想让这个类型成为一种
union来表示其中的
要么-要么部分。此外,联合类型
{a: any} | {b: any}
不够严格,因为在TypeScript中,类型是开放/可扩展的,总是可以具有未知的额外属性……这意味着类型不是
exact的。因此,值
{a: 1, b: 2}
与类型
{a: any}
匹配,并且目前在TypeScript中没有支持来具体表示像
Exact<{a: any}>
这样的东西,它允许
{a: 1}
但禁止
{a: 1, b: 2}
。
话虽如此,TypeScript确实具有
过度属性检查,其中
对象字面量被视为精确类型。这在
check
情况下对您有用(错误“对象文字只能指定已知属性”特别是过度属性检查的结果)。但是,在
check2
情况下,相关类型将是像
{a: any} | {b: any}
这样的联合类型...由于
a
和
b
都至少在联合的一个成员中存在,因此过度属性检查不会在那里启动,至少在TS3.5时是这样。那被认为是
一个bug;假设
{a: 1, b: 2}
应该在每个联合成员都有多余属性时失败过度属性检查。但是目前还不清楚何时甚至是否会解决这个问题。
无论如何,最好让
OneKey<"a" | "b">
评估为一种类型,例如
{a: any, b?: never} | {a?: never, b: any}
...类型
{a: any, b?: never}
将匹配
{a: 1}
,因为
b
是可选的,但不会匹配
{a: 1, b: 2}
,因为
2
不能赋值给
never
。这将给你想要的
但不是两者都有的行为。
在我们开始编码之前,还有一件事:类型
{k?: never}
等效于类型
{k?: undefined}
,因为可选属性总是可以有一个
undefined
值(并且TypeScript不能很好地
区分缺失和undefined
)。
以下是我可能会这样做的方式:
type OneKey<K extends string, V = any> = {
[P in K]: (Record<P, V> &
Partial<Record<Exclude<K, P>, never>>) extends infer O
? { [Q in keyof O]: O[Q] }
: never
}[K];
我允许
V
成为除了
any
之外的某些值类型,如果您想要特别使用
number
或其他类型,则可以这样做,但它将默认为
any
。它的工作原理是使用
映射类型来迭代
K
中的每个值
P
并为每个值生成一个属性。该属性本质上是
Record<P,V>
(因此它具有
P
键),与
Partial<Record<Exclude<K,P>,never>>
相交……
Exclude
从联合类型中删除成员,因此
Record<Exclude<K,P>,never>
是一个对象类型,其具有
K
中的每个键,
除了P
,其属性为
never
。而
Partial
使键变成可选。
这个类型 Record<P, V> & Partial<Record<Exclude<K, P>, never>>
看起来很丑,因此我使用了条件类型推断技巧使其变得更加美观... T extends infer U ? {[K in keyof U]: U[K]} : never
会接受一个类型 T
,将其"复制"到一个类型 U
中,然后显式地遍历它的属性。它会将像 {x: string} & {y: number}
这样的类型折叠成 {x: string; y: number}
。
最后,映射类型 {[P in K]: ...}
本身并不是我们想要的;我们需要将其值类型作为联合类型,因此我们通过 {[P in K]: ...}[K]
来进行查找。
请注意,您的 create()
函数应该像这样进行类型定义:
declare function create<K extends string>(s: K): OneKey<K>;
没有T
的话,让我们来测试一下:
const a = "a";
const res = create(a);
所以,
res
仍然是类型为
{a: any}
的对象,而且行为与您期望的相同。
const check: typeof res = { a: 1, b: 2 };
现在,我们有了这个:
declare const many: "a" | "b";
const res2 = create(many);
所以这就是我们想要的联合。它解决了你的check2
问题吗?
const check2: typeof res2 = { a: 1, b: 2 };
{{是的!}}
需要注意的一点是:如果传递给create()
的参数只是一个string
而不是字符串字面量的联合类型,那么结果类型将具有字符串索引签名并且可以接受任意数量的键:
declare const s: string
const beware = create(s)
const b: typeof beware = {a: 1, b: 2, c: 3};
无法在 TypeScript 中通过字符串进行分发,因此无法表示类型“一个来自所有可能字符串字面量集合中的单个键的对象类型”。您可以可能将 create() 更改为不允许类型为字符串的参数,但是答案已经足够长了。如果您足够关心并想尝试解决这个问题,则由您决定。
好的,希望这能有所帮助;祝你好运!
链接到代码
Object.keys(yourObj).length
应该可以解决问题 :) - Gibor