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

7

我希望定义一种只能有一个键的对象类型。

以下是尝试:

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"

为什么不创建一个接口呢?或者像一个工厂方法一样,它将验证类型并在其不是字符串(或抛出错误等)时返回“未定义”,否则返回所需的对象? - Gibor
1
我对如何进行静态检查以确保对象只有一个键感兴趣。 - Max Heiber
Object.keys(yourObj).length 应该可以解决问题 :) - Gibor
@MaxHeiber 你能描述一下你使用这个的场景吗?对我来说似乎有些违反直觉。 - Nenad
可能是typescript restrict number of object's properties的重复问题,还有这个链接:https://github.com/Microsoft/TypeScript/issues/10667 - DAG
@DaGardner 谢谢,我标记为重复。 - Max Heiber
1个回答

22
如果我理解正确,你想让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确实具有过度属性检查,其中对象字面量被视为精确类型。这在check情况下对您有用(错误“对象文字只能指定已知属性”特别是过度属性检查的结果)。但是,在check2情况下,相关类型将是像{a: any} | {b: any}这样的联合类型...由于ab都至少在联合的一个成员中存在,因此过度属性检查不会在那里启动,至少在TS3.5时是这样。那被认为是一个bug;假设{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

无法在 TypeScript 中通过字符串进行分发,因此无法表示类型“一个来自所有可能字符串字面量集合中的单个键的对象类型”。您可以可能将 create() 更改为不允许类型为字符串的参数,但是答案已经足够长了。如果您足够关心并想尝试解决这个问题,则由您决定。

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

链接到代码


9
感谢您的回复。我认为我正在寻找的是禁止工会。显然,我是资本主义者。 - Max Heiber
但是 declare const many: "a" | "b"; const res2 = create(many); 是特别使用联合类型。你想让 那一行 出现错误吗? - jcalz
也许,如果create(many)给我一个由每个仅有一个键的对象类型组成的联合体,那就更好了。在这种情况下,我想要的是限制每个对象只能有一个键 - 无论create(many)在这种情况下分布方式如何或者出现错误都可以。也许以不同的方式分布会更好。 - Max Heiber
好吧,我想这个答案就是“分布不同”的答案,我也可以想出一种方法,如果“many”是联合类型,那么create(many)会产生编译器错误。但是现在问题被标记为重复,并且它所谓的重复问题根本没有涉及到这些问题,因此在那里添加这样的答案似乎是离题的。我不确定继续进行是否有任何意义,除非您想重新打开这个问题。 - jcalz
当然可以(https://repl.it/@jcalz/WhimsicalHappyCensorware),尽管可能比您想要的更丑。 (我假设您希望K是单个字符串文字值,以及联合或string - jcalz
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接