首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >TypeScript:只有一个键的对象的类型(不允许作为键的联合类型)

TypeScript:只有一个键的对象的类型(不允许作为键的联合类型)
EN

Stack Overflow用户
提问于 2019-08-20 10:31:01
回答 1查看 11.9K关注 0票数 6

我希望定义一个对象的type,该对象可以有一个键。

以下是一次尝试:

代码语言:javascript
复制
type OneKey<K extends string> = Record<K, any>

不幸的是,这并不完全有效,因为变量可以有一个联合类型:

代码语言:javascript
复制
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"
EN

回答 1

Stack Overflow用户

发布于 2019-08-20 15:12:24

如果我正确理解,您希望OneKey<"a" | "b">类似于{a: any, b?: never} | {a?: never, b: any}。这意味着它要么有a键,要么有b键,但两者都有。因此,您希望类型是某种类型的友联市,以表示其中的任何一种或部分。此外,联合类型{a: any} | {b: any}没有足够的限制,因为TypeScript中的类型是可打开/可扩展的,并且总是具有未知的额外属性.意思是类型不是精确。因此,值{a: 1, b: 2}确实与{a: any}类型相匹配,而且目前TypeScript中不支持具体表示类似于Exact<{a: any}>的东西,它允许{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。这会给你你想要的但不是-两种行为。

在我们开始使用代码之前,最后一件事是:类型{k?: never}等同于类型{k?: undefined},因为可选属性总是有一个undefined值(而TypeScript没有很好地完成undefined的工作)。

我可以这样做:

代码语言:javascript
复制
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()函数应该输入如下所示:

代码语言:javascript
复制
declare function create<K extends string>(s: K): OneKey<K>;

没有那个T在里面。让我们来测试一下:

代码语言:javascript
复制
const a = "a";
const res = create(a);
// const res: { a: any; }

因此,res仍然是您想要的{a: any}类型,并且行为相同:

代码语言:javascript
复制
// Good
const check: typeof res = { a: 1, b: 2 };
//                                ~~ Error, object may only specify known properties

然而,现在,我们有了这样的情况:

代码语言:javascript
复制
declare const many: "a" | "b";
const res2 = create(many);
// const res2: { a: any; b?: undefined; } | { b: any; a?: undefined; }

这就是我们想要的工会。它能解决你的check2问题吗?

代码语言:javascript
复制
const check2: typeof res2 = { a: 1, b: 2 }; // error, as desired
//    ~~~~~~ <-- Type 'number' is not assignable to type 'undefined'.

是!

需要注意的一点是:如果create()的参数只是一个string,而不是字符串文本的联合,那么得到的类型将有一个字符串索引签名,并且可以接受任意数量的键:

代码语言:javascript
复制
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中表示“来自所有可能的字符串文本集的具有单一键的对象类型”。您可能会将create()更改为不允许string类型的参数,但是这个答案足够长。如果你足够在意去处理这件事,那就看你的了。

好吧,希望这能帮上忙,祝你好运!

链接到代码

票数 17
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/57571664

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档