首页 > 解决方案 > TypeScript:只有一个键的对象的类型(不允许联合类型作为键)

问题描述

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

标签: typescriptunion-types

解决方案


如果我理解正确,你想OneKey<"a" | "b">成为{a: any, b?: never} | {a?: never, b: any}. 这意味着它要么有一个akey要么有一个bkey但不是两者都有。所以你希望类型是某种联合来代表它的非此即彼的一部分。此外,联合类型{a: any} | {b: any}的限制性不够,因为 TypeScript 中的类型是开放/可扩展的,并且总是可以具有未知的额外属性……意味着类型不精确。因此该值{a: 1, b: 2}确实与 type 匹配{a: any},并且目前 TypeScript 中不支持具体表示类似于Exact<{a: any}>which allowed{a: 1}但禁止的内容{a: 1, b: 2}

话虽这么说,TypeScript 确实有多余的属性检查,其中对象文字被视为具有精确类型。在这种情况下这对您check有用(错误“对象文字可能只指定已知属性”特别是过度属性检查的结果)。但在这种check2情况下,相关类型将是一个类似{a: any} | {b: any}... 的联合,并且由于两者ab都存在于联合的至少一个成员中,因此至少从 TS3.5 开始,不会进行过多的属性检查。这被认为是一个错误;大概{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。这会给你你想要的但不是两种行为。

在我们开始编写代码之前的最后一件事: type{k?: never}等同于 type {k?: undefined},因为可选属性总是可以有一个undefined值(并且 TypeScript 并不能很好地区分 missing 和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成为某种值类型,但它会默认为. 它的工作方式是使用映射类型来迭代每个值并为每个值生成一个属性。这个属性本质上(所以它确实有一个键)与...相交从联合中删除成员,因此每个键都在except中的对象类型,并且其属性是。并且使键可选。 anynumberanyPKRecord<P, V>PPartial<Record<Exclude<K, P>, never>>ExcludeRecord<Exclude<K, P>, never>K PneverPartial

该类型很难看Record<P, V> & Partial<Record<Exclude<K, P>, never>>,因此我使用条件类型推断技巧使其再次变得漂亮……T extends infer U ? {[K in keyof U]: U[K]} : never将采用 type T,将其“复制”到 type U,然后显式迭代其属性。它将采用 like 类型{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);
// 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()只是 astring而不是字符串文字的并集,则生成的类型将具有字符串索引签名并且可以采用任意数量的键:

declare const s: string
const beware = create(s) // {[k: string]: any}
const b: typeof beware = {a: 1, b: 2, c: 3}; // no error

没有办法分布在string中,因此无法在 TypeScript 中表示类型“具有所有可能字符串文字集中的单个键的对象类型”。您可能会更改为禁止 type 的参数,但这个答案已经足够长了。如果您足够关心并尝试处理它,这取决于您。create()string


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

链接到代码


推荐阅读