如何在TypeScript中使用已知键和未知键键入对象

Jac*_*pie 6 typescript conditional-types

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

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

const comboObject: ComboObject = {
  known: true
  field: 123
  unknownName: 'value'
}
Run Code Online (Sandbox Code Playgroud)

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

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

interface ComboObject {
  [U: string]: boolean | number | string
}
Run Code Online (Sandbox Code Playgroud)

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

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

bra*_*ipt 11

在我看来,更简单的方法是使用交叉类型:

type ComboObject = {
  known: boolean
  field: number
} & {
  [key: string]: string | number | boolean;
};
Run Code Online (Sandbox Code Playgroud)

这告诉 TypeScript 从左侧继承,但当你向它提供额外的未知参数类型时,它不会生气。

一个稍微冗长和复杂的解决方案,但可以通过显式Exclude调用已知类型来提供一些额外的约束:

type ComboObject = {
  known: boolean;
  field: number;
} & Record<Exclude<string, "known" | "field">, string | number | boolean>;
Run Code Online (Sandbox Code Playgroud)
type ComboObject = {
  known: boolean
  field: number
} & {
  [key: string]: string | number | boolean;
};
Run Code Online (Sandbox Code Playgroud)


jca*_*alz 8

你自找的。

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

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

然后使用它来检测给定string类型是否为单个字符串文字(因此:not string,not never和not union):

type IsASingleStringLiteral<
  T extends string,
  Y = true,
  N = false
> = string extends T ? N : [T] extends [never] ? N : IsAUnion<T, N, Y>;
Run Code Online (Sandbox Code Playgroud)

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

type BaseObject = { known: boolean, field: number };
Run Code Online (Sandbox Code Playgroud)

为准备错误消息,我们定义一个,ProperComboObject以便当您搞砸时,错误提示您应该执行的操作:

interface ProperComboObject extends BaseObject {
  '!!!ExactlyOneOtherStringPropertyNoMoreNoLess!!!': string
}
Run Code Online (Sandbox Code Playgroud)

这是主要课程。 VerifyComboObject<C>接受一个类型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;
Run Code Online (Sandbox Code Playgroud)

它通过解剖CBaseObject,其余的按键X。如果C不匹配BaseObject & Record<X, string>,则您失败了,因为这意味着它不是BaseObject,或者是具有额外非string属性的一个。然后,通过与进行检查,确保剩下一个密钥。 XIsASingleStringLiteral<X>

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

const asComboObject = <C>(x: C & VerifyComboObject<C>): C => x;
Run Code Online (Sandbox Code Playgroud)

让我们测试一下:

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
Run Code Online (Sandbox Code Playgroud)

第一个根据需要进行编译。最后三个失败的原因与额外属性的数量和类型有关。该错误消息有点神秘,但这是我能做的最好的事情。

您可以在Playground中查看运行中的代码。


同样,我不建议将其用于生产代码。我喜欢使用类型系统,但是这种系统特别复杂且脆弱,我也不想为任何无法预料的后果负责。

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

  • @jcalz 从最新版本开始,在 TypeScript 中没有更简单的方法可以做到这一点吗?我真的只想要类似的东西:`{foo: number; [键:notFoo]:字符串}` (3认同)
  • 如果您希望“notFoo”成为*单个*字符串键,上面的方法可能仍然是我的做法。如果您只想让“notFoo”成为“foo”以外的任何键或键集,那么您仍然需要使用通用映射条件类型,但它更简单一些。直到且除非 TypeScript 实现[任意索引签名类型](https://github.com/Microsoft/TypeScript/pull/26797) 和[否定类型](https://github.com/microsoft/TypeScript/pull/29317) ,没有具体的方式来表示 `{foo: number; [k:字符串而不是“foo”]:字符串}`。 (2认同)

hug*_*o00 6

不错的@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
     },
});
Run Code Online (Sandbox Code Playgroud)

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