【问题标题】:How do I type an object with known and unknown keys in TypeScript如何在 TypeScript 中键入具有已知键和未知键的对象
【发布时间】:2018-04-22 18:33:19
【问题描述】:

我正在寻找一种方法来为以下具有两个已知键和一个具有已知类型的未知键的对象创建 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 条件类型有帮助吗?

【问题讨论】:

  • TypeScript 不适合这种情况。我有一种方法可以强制编译器(使用条件类型)将函数参数限制为与您预期的 ComboObject 类型匹配的类型(恰好是一个带有字符串属性且没有其他属性的额外键),但这太可怕了,而不是你的东西d 想在任何生产代码中使用。如果您有兴趣,我可以发布它,但我认为您可能想要寻求其他更适合 TypeScript 的选项。
  • @jcalz 是的,如果您可以发布或以其他方式发送它会很棒,即使它不完全可行,它也可能会激发一些想法。
  • 这是一个非常特殊的情况,您需要 3 个属性,2 个已知属性和 1 个未知属性。 ????。我有一种情况,我有一个已知属性,然后是任意数量的未知属性(字符串 ????s)。我确实喜欢这个:props: { children: string[] | VDOMElement[] } & { [key: string]: string }; 但是,在没有 & 运算符的情况下将它们放在一起 没有 工作:props: { children: string[] | VDOMElement[], [key: string]: string };
  • @CodeFinity 即使使用通常不起作用的& {[key: string]: string} 方法,因为已知属性不能与字符串联合:typescriptlang.org/play?#code/…
  • 好吧,我的实际代码中没有红线……但是。 ??? ??????而且,不,我不是通过降低tsconfig 来“作弊”。 :) 非常严格。

标签: typescript conditional-types


【解决方案1】:

你要求的。

让我们做一些类型操作来检测给定类型是否是联合。它的工作方式是使用条件类型的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&lt;C&gt; 采用 C 类型,如果它符合您想要的 ComboObject 类型,则原封不动地返回它;否则它会返回 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。如果CBaseObject &amp; Record&lt;X, string&gt; 不匹配,那么您就失败了,因为这意味着它要么不是BaseObject,要么是具有额外的非string 属性的。然后,通过检查XIsASingleStringLiteral&lt;X&gt;,确保正好有一个剩余密钥。

现在我们创建一个辅助函数,它要求输入参数匹配VerifyComboObject&lt;C&gt;,并返回输入不变。如果您只想要一个正确类型的对象,它可以让您及早发现错误。或者您可以使用签名来帮助您自己的函数需要正确的类型:

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,我不想对任何unforeseen consequences负责。

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

【讨论】:

  • 这太棒了,谢谢!它有助于理解类型系统是如何工作的。最后一个问题,未知键是否可能具有接口类型而不是字符串类型?我本以为可以通过将Record&lt;X, string&gt; 更改为Record&lt;X, InterfaceType&gt; 来实现,但这似乎允许界面中的任意属性。
  • “任意属性”是什么意思?它不会允许您遗漏属性或给它们错误的类型,对吗?如果您的意思是多余的属性,是的,这不足为奇。过多的属性检查只发生在 TypeScript 中的特定位置。它没有真正的exact types,并且强制编译器拒绝多余的属性将需要另一轮type golf
  • 有趣,你是对的,它允许附加属性,但正确地检查给定属性的类型。感谢您提供指向 GitHub 的链接,这也解释了您将如何获得完全相同的行为。
  • 此示例从 TypeScript 3.5.1 开始失败,并出现错误“Type 'keyof C' is not assignable to type 'string'”
  • @jcalz 在最新版本的 TypeScript 中没有更简单的方法吗?我真的只是想要这样的东西:{foo: number; [key: notFoo]: string}
【解决方案2】:

不错的@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
     },
});

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

ty

【讨论】:

    猜你喜欢
    • 2020-06-17
    • 2021-01-18
    • 1970-01-01
    • 1970-01-01
    • 2021-10-21
    • 1970-01-01
    • 2022-06-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多