如何在 TypeScript 中键入具有已知和未知键的对象

How do I type an object with known and unknown keys in TypeScript

我正在寻找一种方法来为以下对象创建 TypeScript 类型,该对象具有两个已知键和一个具有已知类型的未知键:

interface ComboObject {
  known: boolean
  field: number
  [U: string]: string
}

const comboObject: ComboObject = {
  known: true
  field: 123
  unknownName: 'value'
}

该代码不起作用,因为 TypeScript 要求所有属性都匹配给定索引签名的类型。但是,我不想使用索引签名,我想输入一个我知道其类型但不知道其名称的字段。

到目前为止我唯一的解决方案是使用索引签名并设置所有可能类型的联合类型:

interface ComboObject {
  [U: string]: boolean | number | string
}

但这有很多缺点,包括在已知字段上允许不正确的类型以及允许任意数量的未知键。

有没有更好的方法? TypeScript 2.8 条件类型有帮助吗?

你自找的。

让我们做一些类型操作来检测给定类型是否为联合。它的工作方式是使用条件类型的 distributive 属性 将联合展开到成分,然后注意每个成分都比联合窄。如果不是这样,那是因为联盟只有一个组成部分(所以它不是联盟):

type IsAUnion<T, Y = true, N = false, U = T> = U extends any
  ? ([T] extends [U] ? N : Y)
  : never;

然后用它来检测给定的 string 类型是否是单个字符串文字(所以:不是 string,不是 never,也不是联合):

type IsASingleStringLiteral<
  T extends string,
  Y = true,
  N = false
> = string extends T ? N : [T] extends [never] ? N : IsAUnion<T, N, Y>;

现在我们可以开始处理您的特定问题了。将 BaseObject 定义为 ComboObject 的一部分,您可以直接定义:

type BaseObject = { known: boolean, field: number };

并为错误消息做准备,让我们定义一个 ProperComboObject 以便当您搞砸时,该错误会提示您应该做什么:

interface ProperComboObject extends BaseObject {
  '!!!ExactlyOneOtherStringPropertyNoMoreNoLess!!!': string
}

主菜来了。 VerifyComboObject<C> 采用 C 类型,如果符合您想要的 ComboObject 类型,则 returns 保持不变;否则它 returns ProperComboObject (它也不符合)错误。

type VerifyComboObject<
  C,
  X extends string = Extract<Exclude<keyof C, keyof BaseObject>, string>
> = C extends BaseObject & Record<X, string>
  ? IsASingleStringLiteral<X, C, ProperComboObject>
  : ProperComboObject;

它的工作原理是将 C 分解为 BaseObject 和剩余的键 X。如果 C 不匹配 BaseObject & Record<X, string>,那么您就失败了,因为这意味着它不是 BaseObject,或者它具有额外的非 string 属性。然后,它通过检查 XIsASingleStringLiteral<X>.

来确保 正好有一个 剩余密钥

现在我们做一个辅助函数,要求输入参数匹配VerifyComboObject<C>,并且returns输入不变。如果您只想要正确类型的对象,它可以让您尽早发现错误。或者您可以使用签名来帮助使您自己的函数需要正确的类型:

const asComboObject = <C>(x: C & VerifyComboObject<C>): C => x;

我们来测试一下:

const okayComboObject = asComboObject({
  known: true,
  field: 123,
  unknownName: 'value'
}); // okay

const wrongExtraKey = asComboObject({
  known: true,
  field: 123,
  unknownName: 3
}); // error, '!!!ExactlyOneOtherStringPropertyNoMoreNoLess!!!' is missing

const missingExtraKey = asComboObject({
  known: true,
  field: 123
}); // error, '!!!ExactlyOneOtherStringPropertyNoMoreNoLess!!!' is missing

const tooManyExtraKeys = asComboObject({
  known: true,
  field: 123,
  unknownName: 'value',
  anAdditionalName: 'value'
}); // error, '!!!ExactlyOneOtherStringPropertyNoMoreNoLess!!!' is missing

根据需要编译第一个。最后三个因与额外属性的数量和类型有关的不同原因而失败。错误消息有点含糊,但这是我能做的最好的了。

您可以在 the Playground 中查看实际代码。


同样,我认为我不建议将其用于生产代码。我喜欢玩类型系统,但是这个感觉特别 complicated and fragile, and I wouldn't want to feel responsible for any unforeseen consequences.

希望对您有所帮助。祝你好运!

不错的@jcalz

它给了我一些很好的洞察力,让我可以到达我想要的地方。 我喜欢一个具有一些已知属性的 BaseObject,并且 BaseObject 可以拥有任意数量的 BaseObject。

type BaseObject = { known: boolean, field: number };
type CoolType<C, X extends string | number | symbol = Exclude<keyof C, keyof BaseObject>> = BaseObject & Record<X, BaseObject>;
const asComboObject = <C>(x: C & CoolType<C>): C => x;

const tooManyExtraKeys = asComboObject({
     known: true,
     field: 123,
     unknownName: {
         known: false,
         field: 333
     },
     anAdditionalName: {
         known: true,
         field: 444
     },
});

这样我就可以对已有的结构进行类型检查,而无需进行太多更改。