我希望定义一个type
对象,它们可以只有一个键。
这是尝试:
type OneKey<K extends string> = Record<K, any>
不幸的是,由于变量可以具有并集类型,因此这不太起作用:
type OneKey<K extends string> = Record<K, any>
declare function create<
K extends string,
T extends OneKey<K>[K]
>(s: K): OneKey<K>
const a = "a";
const res = create(a);
// Good
const check: typeof res = { a: 1, b: 2 }
// ~~ Error, object may only specify known properties
declare const many: "a" | "b";
const res2 = create(many);
// **Bad**: I only want one key
const check2: typeof res2 = { a: 1, b: 2 }; // No error
declare const x: "k1" | "k2"
答案 0 :(得分:2)
如果我的理解正确,那么您希望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确实具有excess property checking,其中对象文字被视为完全相同。在check
情况下,这适用于您(错误“对象文字只能指定已知属性”是由于过多的属性检查而导致的)。但是在check2
情况下,相关类型将是像{a: any} | {b: any}
...这样的联合,并且由于a
和b
都存在于至少一个工会,至少从TS3.5开始不会进行多余的财产检查。那是considered a bug;大概{a: 1, b: 2}
应该会通过多余属性检查,因为它对联合的每个成员都有多余的属性。但是尚不清楚何时或什至将解决该错误。
在任何情况下,最好将OneKey<"a" | "b">
评估为类似{a: any, b?: never} | {a?: never, b: any}
的类型...类型{a: any, b?: never}
将匹配{a: 1}
,因为{{1} }是可选的,但不是b
,因为{a: 1, b: 2}
无法分配给never
。这将为您提供您想要的但并非全部行为。
在开始使用代码之前的最后一件事:类型2
等同于类型{k?: never}
,因为可选属性始终可以具有{k?: undefined}
值(而TypeScript不能distinguishing missing from undefined
做得很好。
这就是我的做法:
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
。它的工作方式是使用mapped type遍历any
中的每个值P
并为每个值产生一个属性。此属性本质上是K
(因此它确实有一个Record<P, V>
键)与P
相交... Exclude
从联合中删除成员,因此Partial<Record<Exclude<K, P>, never>>
是一种对象类型,其中每个键都位于Record<Exclude<K, P>, never>
中,除了 K
,其属性为P
。 never
使键成为可选键。
类型Partial
很丑陋,所以我使用了一个conditional type inference技巧来使它再次漂亮... 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]: ...}
lookup来获取这些值。
请注意,您的{[P in K]: ...}[K]
函数应按以下类型输入:
create()
其中没有declare function create<K extends string>(s: K): OneKey<K>;
。让我们测试一下:
T
所以const a = "a";
const res = create(a);
// const res: { a: any; }
仍然是您想要的类型res
,并且具有相同的行为:
{a: any}
现在,我们有了这个:
// Good
const check: typeof res = { a: 1, b: 2 };
// ~~ Error, object may only specify known properties
这就是我们想要的工会。它可以解决您的declare const many: "a" | "b";
const res2 = create(many);
// const res2: { a: any; b?: undefined; } | { b: any; a?: undefined; }
问题吗?
check2
是的!
需要考虑的一个警告:如果const check2: typeof res2 = { a: 1, b: 2 }; // error, as desired
// ~~~~~~ <-- Type 'number' is not assignable to type 'undefined'.
的参数只是一个create()
而不是字符串文字的并集,则结果类型将具有字符串索引签名并且可以采用任意数量的键:
string
无法在declare const s: string
const beware = create(s) // {[k: string]: any}
const b: typeof beware = {a: 1, b: 2, c: 3}; // no error
上进行分配,因此无法在TypeScript中表示类型“对象类型,该对象类型来自所有可能的字符串集中的单键文字”。您可以更改string
以禁止使用create()
类型的参数,但是这个答案就足够了。如果您足够关心尝试解决该问题,则取决于您。
好的,希望能有所帮助;祝你好运!