问题是

如何使用一个字段键入对象,其中每个键只能是数组中对象的id属性,而数组是另一个字段?(使用TypeScrip)

这个问题一开始可能有点难理解,所以这里有一个例子:

const x = {
  fields: [
    {
      id: "field1",
      type: "string"
    },
    {
      id: "field2",
      type: "number"
    },
    {
      id: "my_field",
      type: "email"
    }
  ],
  fieldValues: {
    field1: "John",
    field2: "John",
    my_field: "@"
  }
}

所以问题是,我如何为x创建一个类型,以便判断fieldValues是否所有的键都是fields中的id之一?

我到目前为止的收获

拿第一名

以下代码片段按预期工作,但每个id都需要as const修饰符,并且必须分别定义fieldsfieldValues.

const fields = [
  {
    id: "bid" as const,
    c: "d"
  },
  {
    id: "cid" as const,
    c: "e"
  }
]

const fieldValues: Record<typeof fields[number]["id"], any> = {
  bid: "d",
  cid: "e",
  // The next line errors, yay!
  a: "f"
}

拿2号吧

下面的代码片段近乎完美.然而,测试需要显式定义ID,这是不可取的.完美的解决方案如下所示,但不需要显式提供这些ID.

interface FieldType {
  type: "string" | "number" | "email"
}

type ArrayElement<ArrayType extends readonly unknown[]> =
  ArrayType extends readonly (infer ElementType)[] ? ElementType : never

type Fields<T extends string[]> = readonly (FieldType & {
  id: ArrayElement<T>
})[]

interface Test<T extends string[]> {
  fields: Fields<T>
  fieldValues: Record<ArrayElement<Fields<T>>["id"], any>
}

const x: Test<["field1", "field2", "my_field"]> = {
  fields: [
    {
      id: "field1",
      type: "string"
    },
    {
      id: "field2",
      type: "number"
    },
    {
      id: "my_field",
      type: "email"
    }
  ],
  fieldValues: {
    field1: "John",
    field2: "John",
    my_field: "@"
  }
}

推荐答案

Typescript 中没有特定的Test种文字可以按照你想要的方式工作.您可以编写一个generic类型Test<K>,其中K表示特定对象的literal types个键中的union个.

但是为了让Test成为一个特定的类型,你基本上需要说:"someK中的Test<K>我不在乎".这将需要microsoft/TypeScript#14466中所要求的所谓的100.如果这些类型存在于类型脚本中,您可能会编写type Test = <exists K> GenericTest<K>,但它们不存在于类型脚本中(在大多数其他语言中也不存在,这不是类型脚本的一个特殊缺陷),所以您不能这样做.有一些方法可以在类型脚本中编码这样的类型,但它们相当于类似Promise的抽象,并且只对某些用例有用.

所以没有办法写

const x: Test = ⋯;

直接go 吧.相反,我们需要涉及泛型.


让我们看一下泛型类型Test<K>.根据您的描述,我建议使用比您的版本更简单的方法,不需要使用conditional types或try 拆分数组:

type Fields<K extends string> = readonly ({
  type: "string" | "number" | "email"
  id: K
})[]

interface Test<K extends string> {
  fields: Fields<K>
  fieldValues: Record<K, any>
}

然后你的x值将是Test<"field1" | "field2" | "my_field">类型.现在,如果你能写一些像这样的东西就好了

// not valid TS, don't try this
const x: Test<infer> = {⋯}

让编译器为您推断泛型类型参数K,而不是强制您将其写出来.对于这一点,有一个长期开放的功能请求,但到目前为止,它还不是语言的一部分.目前,只有在调用泛型function时才能推断泛型类型参数.

这意味着我们可以编写一个小的实用帮助函数test()来返回它的输入,通过调用该函数,类型参数被推断出来. 所以看起来

const x = test({⋯});

然后x仍然是Test<"field1" | "field2" | "my_field">类型,而不需要你自己写出来.该函数非常简单:

const test = <K extends string>(
  t: Test<K>) => t;

这是可行的,因为我编写Test<K>的方式使得可以很容易地从类型Test<K>的值中推断出K.让我们来演示一下:

const x = test({
  fields: [
    { id: "field1", type: "string" },
    { id: "field2", type: "number" },
    { id: "my_field", type: "email" }
  ],
  fieldValues: {
    field1: "John",
    field2: "John",
    my_field: "@",
  }
});

type X = typeof x;
//   ^? type X = Test<"field1" | "field2" | "my_field">

看上go 不错.请注意,我们可以使用the TypeScript typeof operator来获得x类型的名称,而不必写出类型参数.因此,上面的代码展示了如何获取一个对象,让编译器验证它与Test匹配,然后获取它的类型.

它只表明有效对象有效.让我们看看无效对象会发生什么:

const badX1 = test({
  fields: [
    { id: "field1", type: "string" },
    { id: "field2", type: "number" },
    { id: "my_field", type: "email" }
  ],
  fieldValues: { // error!
    //~~~~~~~ <--
    // Property 'field2' is missing
    field1: "John",
    my_field: "@",
  }
});

const badX2 = test({
  fields: [
    { id: "field1", type: "string" },
    { id: "field2", type: "number" },
    { id: "my_field", type: "email" }
  ],
  fieldValues: {
    field1: "John",
    field2: "John",
    field3: "John", // error!
    //~~~~ 
    // Object literal may only specify known properties
    my_field: "@",
  }
});

因此,如果fieldValues的密钥丢失或多余,您会收到警告.


你只能靠这么近了.如果你真的想隐藏泛型并使其特定,你可以编码一个存在泛型类型. 参见Defining an array of differing generic types in TypeScript和/或Typescript generics: How to implicitly derive the type of a property of an object from another property of that object?,了解有关存在泛型及其在TypeScript中编码的更多信息. 它可能看起来像这样:

type SomeTest = <R>(cb: <K extends string>(t: Test<K>) => R) => R;

const someTest = <K extends string>(t: Test<K>): SomeTest => cb => cb(t);

const someX = someTest({
  fields: [
    { id: "field1", type: "string" },
    { id: "field2", type: "number" },
    { id: "my_field", type: "email" }
  ],
  fieldValues: {
    field1: "John",
    field2: "John",
    my_field: "@",
  }
});

因此,现在someX是一个接受回调的函数,该回调接受任何KTest<K>,并返回该回调的结果.它保持着与x相同的价值,但你永远无法访问它.你只能通过一些函数来touch 它,对于K来说,它是一些不透明的东西.例如,您可以索引到fieldValues:

const str = someX(t => t.fields.map(f => t.fieldValues[f.id])).join(":");
console.log(str) // "John:John:@"

如果你查看回调函数内部,f.idK类型,t.fieldValuesRecord<K, any>类型,不管K是什么. 所以不管K是什么,你都只能做有意义的操作.你永远无法访问t.fieldValues.field1,因为它只在"field1" extends K时有效,而你不知道K.

如果您需要的是与JSON兼容的类型,这种方法不太可能有帮助,因为函数不是.但对于有其他用例的future 读者来说,这可能是一种可行的方式.通用效用函数方法在很多用例中都很有用.但是,如果用例要求只有一个特定的对象类型表示未知KTest<K>,那么这在TypeScrip中是不可能的,除非语言中添加了其他特性.

Playground link to code

Typescript相关问答推荐

使用新控制流的FormArray内部的Angular FormGroup

如何将绑定到实例的类方法复制到类型脚本中的普通对象?

Typescript:将内部函数参数推断为子对象键的类型

ReturnType此索引方法与点表示法之间的差异

如果我有对象的Typescript类型,如何使用其值的类型?

处理API响应中的日期对象

我应该使用什么类型的React表单onsubmit事件?

类型{...}的TypeScript参数不能赋给类型为never的参数''

为什么typescript不能推断类的方法返回类型为泛型''

状态更新后未触发特定元素的Reaction CSS转换

TypeScrip:逐个 case Select 退出noUncheck kedIndexedAccess

为什么有条件渲染会导致material 自动完成中出现奇怪的动画?

TypeScrip省略了类型参数,仅当传递另一个前面的类型参数时才采用默认类型

基元类型按引用传递的解决方法

如何在React组件中指定forwardRef是可选的?

如何编辑切片器以使用CRUD调用函数?

如何连接属性名称和值?

TypeScrip:使用Cypress测试包含插槽的Vue3组件

如何创建一个将嵌套类属性的点表示法生成为字符串文字的类型?

为什么我会得到一个;并不是所有的代码路径都返回一个值0;switch 中的所有情况何时返回或抛出?(来自vsCode/TypeScript)