我正在try 创建一个Reaction组件,该组件基于Configuration对象呈现输入表,该对象定义了字段属性名称及其输入类型和初始数据行.

以下是预期用途的示例:

const render = <C extends Config>(config: C, initialData: Row<C>[]) => {
  return initialData
}

const config = {
  properties: [
    {
      input: InputType.Text,
      label: 'Crop',
      name: 'crop',
    },
    {
      input: InputType.Number,
      label: 'Yield(t)',
      name: 'yield_t',
    },
  ],
}

const initialData = [
  { crop: 'corn', yield_t: 2 },
  { crop: 'barley', yield_t: 2 },
  { crop: 2, yield_t: 'ten' }, // ❌ bad input
]

render(config, initialData)

我正在try 得到像上面这样的例子,输入错误,让Typescript 抱怨.

以下是我到目前为止所拥有的(also in a TS playground)

/**
 * WIP:
 *
 * An attempt to create typescript-level type validation of
 * initialData dependent on the properties defined in config.
 */
enum InputType {
  Text = 'Text',
  Number = 'Number',
  Date = 'Date',
}

export type FieldConfig = {
  input: InputType
  label: string
  name: string
}

export type Config = {
  properties: ReadonlyArray<FieldConfig>
}

export type Row<C extends Config> = {
  [K in Names<C>]: GetFieldType<Extract<Properties<C>, { name: K }>['input']>
}

/* Extracts a union of properties from a config type,
 * i.e. { name: 'crop', ... } | { name: 'yield_t', ... } */
type Properties<C extends Config> = C['properties'][number]

/* Gets the names of of the properties from a config type,
 * i.e. 'crop' | 'yield_t' */
type Names<C extends Config> = C['properties'][number]['name']

/**
 * Gets the scalar type of an Input Type
 */
type GetFieldType<Type extends keyof typeof InputType> =
  Type extends InputType.Text
    ? string
    : Type extends InputType.Date
      ? string
      : Type extends InputType.Number
        ? number
        : never

/**
 * Converts an union of Properties into a mapped object type, i.e.:
 *
 * properties: [{ name: 'crop', ... }, { name: 'yield_t', ... }]
 * ->
 * {
 *   'crop': { name: 'crop', ... },
 *   'yield_t': { name: 'yield_t', ... },
 * }
 */
type NameMap<C extends Config> = {
  [PropertyName in Names<C>]: Extract<Properties<C>, { name: PropertyName }>
}

const render = <C extends Config>(config: C, initialData: Row<C>[]) => {
  return initialData
}

const config = {
  properties: [
    {
      input: InputType.Text,
      label: 'Crop',
      name: 'crop',
    },
    {
      input: InputType.Number,
      label: 'Yield(t)',
      name: 'yield_t',
    },
  ],
}

const initialData: Row<typeof config>[] = [
  { crop: 'corn', yield_t: 2 },
  { crop: 'barley', yield_t: 2 },
  // @ts-expect-error this should fail :(
  { crop: 2, yield_t: 'ten' },
]

render(
  config,
  // @ts-expect-error this should fail :(
  initialData,
)

最后,我的Row类型具有通用的字符串键,其属性是可能的输入值的联合:

enter image description here

理想情况下,推断的定义应如下所示:

type MyRow = {
  crop: string;
  yield_t: number
}

推荐答案

在接下来的内容中,我只会处理FieldConfig而不是Config. 而不是采取一个generic参数C extends Config,然后indexing into它喜欢C["properties"][number]得到一个unionFieldConfig,我们将只是开始与F extends FieldConfig作为所需的联合.

我采取的方法是定义一个映射接口

interface FieldTypeMap {
  [InputType.Text]: string;
  [InputType.Number]: number;
  [InputType.Date]: string
}

它允许您查找给定InputTypeindexed access的字段类型.这与您的通用conditional type的表单T extends InputType.Text ? string : ⋯相反.对于类型脚本来说,泛型索引访问比泛型条件类型更容易处理.

现在我们可以将Row<F>定义为:

type Row<F extends FieldConfig> =
  { [T in F as T["name"]]: FieldTypeMap[T["input"]] }

其使用key remapping来迭代F的每个联合成员T,并使用nameinput属性来分别确定键和值.这比试图使用Extract找到F中的正确unions 成员要简单得多.

让我们用您的示例来测试这一部分:

const config = {
  properties: [
    {
      input: InputType.Text,
      label: 'Crop',
      name: 'crop',
    },
    {
      input: InputType.Number,
      label: 'Yield(t)',
      name: 'yield_t',
    },
  ],
} as const;

type MyRow = Row<typeof config["properties"][number]>;
/* type MyRow = {
    crop: string;
    yield_t: number;
} */

请注意,我在config定义上使用了const assertion,以要求类型判断器跟踪所涉及的literal types个字符串属性,特别是name个属性.否则,它只会被视为string,没有希望将它们用作 keys .不管怎么说,MyRow型看起来不错.


现在我们可以用Row<F>来定义render():

const render = <const F extends FieldConfig>(
  config: { properties: readonly F[] },
  initialData: readonly Row<F>[]
) => {
  return initialData
}

我还在F类型参数上添加了const modifier,以便给类型判断器一个提示:如果您为config输入一个对象文字参数,它应该再次跟踪属性的文字类型.

让我们来试一试:

render({
  properties: [
    {
      input: InputType.Text,
      label: 'Crop',
      name: 'crop',
    },
    {
      input: InputType.Number,
      label: 'Yield(t)',
      name: 'yield_t',
    },
  ],
}, [
  { crop: 'corn', yield_t: 2 }, // okay
  { crop: 'barley', yield_t: 2 }, // okay
  { crop: 2, yield_t: 'ten' }, // error!
  //~~~~     ~~~~~~~ <-- Type 'string' is not assignable to type 'number'
  // ^-- Type 'number' is not assignable to type 'string'
])

看起来不错 编译器期望第二个参数是一个包含{crop: string; yield_t: number}个对象的数组,因此最后一个元素是不正确的.

Playground link to code

Typescript相关问答推荐

TypScript手册中的never[]参数类型是什么意思?

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

当我点击外部按钮时,如何打开Html Select 选项菜单?

在配置对象上映射时,类型脚本不正确地推断函数参数

基于平台的重定向,Angular 为16

material 表不渲染任何数据

对数组或REST参数中的多个函数的S参数进行类型判断

Angular 错误NG0303在Angular 项目中使用app.Component.html中的NGFor

参数属性类型定义的REACT-RUTER-DOM中的加载器属性错误

转换器不需要的类型交集

如何在打字角react 式中补齐表格组

如何在try 运行独立的文字脚本Angular 项目时修复Visual Studio中的MODULE_NOT_FOUND

创建一个函数,该函数返回一个行为方式与传入函数相同的函数

如何在具有嵌套数组的接口中允许新属性

无法缩小深度嵌套对象props 的范围

在类型脚本中创建显式不安全的私有类成员访问函数

如何在Type脚本中创建一个只接受两个对象之间具有相同值类型的键的接口?

如何在TypeScript类成员函数中使用筛选后的keyof this?

如何提取具有索引签名的类型中定义的键

在Typescript 无法正常工作的 Three.js 中更新动画中的 GLSL 统一变量