TypeScript:仅具有一个键的对象的类型(不允许将联合类型用作键)

时间:2019-08-20 10:31:01

标签: typescript union-types

我希望定义一个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"

1 个答案:

答案 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} ...这样的联合,并且由于ab都存在于至少一个工会,至少从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]; 或其他名称,则我允许Vany以外的某种值类型,但默认值为number。它的工作方式是使用mapped type遍历any中的每个值P并为每个值产生一个属性。此属性本质上是K(因此它确实有一个Record<P, V>键)与P相交... Exclude从联合中删除成员,因此Partial<Record<Exclude<K, P>, never>>是一种对象类型,其中每个键都位于Record<Exclude<K, P>, never>中,除了 K,其属性为Pnever使键成为可选键。

类型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()类型的参数,但是这个答案就足够了。如果您足够关心尝试解决该问题,则取决于您。


好的,希望能有所帮助;祝你好运!

Link to code