我希望定义一个对象的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"发布于 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}那样的联盟.而且,由于a和b都至少存在于联盟的一个成员中,多余的财产检查不会在那里生效,至少在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的工作)。
我可以这样做:
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);
// 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无法跨string进行分发,因此无法在TypeScript中表示“来自所有可能的字符串文本集的具有单一键的对象类型”。您可能会将create()更改为不允许string类型的参数,但是这个答案足够长。如果你足够在意去处理这件事,那就看你的了。
好吧,希望这能帮上忙,祝你好运!
https://stackoverflow.com/questions/57571664
复制相似问题