Typescript - 根据另一个参数验证一个参数的值

问题描述 投票:0回答:1

我正在尝试创建一个反应组件,该组件根据配置对象呈现输入表,该对象定义字段属性名称及其输入类型以及初始数据行。

这是预期用途的示例:

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)

我正在尝试获取像上面这样的实例,但输入错误,以使 Typescript 抱怨。

这是我到目前为止所拥有的(也在 TS 游乐场

/**
 * 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
类型具有通用字符串键,其属性是可能输入值的并集:

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

type MyRow = {
  crop: string;
  yield_t: number
}
typescript
1个回答
0
投票

接下来我将只处理

FieldConfig
而不是
Config
。我们不再采用 generic 参数
C extends Config
,然后像 C["properties"][number] 那样
 索引到 
来获得 FieldConfig
union
,我们只需从
F extends FieldConfig
作为所需的并集开始。

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

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

它可以让您通过

索引访问
查找给定InputType的字段类型。这与 T extends InputType.Text ? string : ⋯ 形式的通用
条件类型
相反。对于 TypeScript 来说,通用索引访问比通用条件类型更容易处理。

现在我们可以将

Row<F>
定义为:

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

它使用 key remapping 迭代

T
的每个联合成员
F
并使用
name
input
属性分别确定键和值。这比尝试使用
Extract
查找
F
的正确联合成员更简单。

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

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;
} */

请注意,我在 const

 定义上使用了 
config
 断言
,要求类型检查器跟踪所涉及字符串属性的文字类型,尤其是
name
属性。否则它只会被视为
string
并且没有希望将它们用作密钥。不管怎样,
MyRow
类型看起来不错。


现在我们可以用

render()
来定义
Row<F>
:

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

我还在 const

 类型参数上添加了 
F
 修饰符
,以向类型检查器提供提示:如果您为
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 代码链接

© www.soinside.com 2019 - 2024. All rights reserved.