You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TypeScript:基于配置参数验证初始数据的类型约束问题

基于配置对象的React输入表格组件类型校验问题

需求概述

我正在开发一个React组件,它能根据配置对象渲染输入表格。配置对象定义了字段的属性名、输入类型,组件还需要接收初始数据行。我希望初始数据的类型能严格匹配配置中定义的字段类型,无效输入能直接触发TypeScript报错。

预期用法示例

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

const config = {
  properties: [
    {
      input: InputType.Text,
      label: '作物',
      name: 'crop',
    },
    {
      input: InputType.Number,
      label: '产量(t)',
      name: 'yield_t',
    },
  ],
}

const initialData = [
  { crop: '玉米', yield_t: 2 },
  { crop: '大麦', yield_t: 2 },
  { crop: 2, yield_t: '十' }, // ❌ 此处应触发TypeScript报错
]

render(config, initialData)

当前实现代码

/**
 * 工作中:
 * 尝试基于配置中的属性定义,对初始数据进行TypeScript层面的类型校验
 */
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']>
}

/* 从配置类型中提取属性的联合类型,
 * 例如:{ name: 'crop', ... } | { name: 'yield_t', ... } */
type Properties<C extends Config> = C['properties'][number]

/* 获取配置类型中所有属性的名称,
 * 例如:'crop' | 'yield_t' */
type Names<C extends Config> = C['properties'][number]['name']

/**
 * 根据输入类型获取对应的标量类型
 */
type GetFieldType<Type extends keyof typeof InputType> =
  Type extends InputType.Text
    ? string
    : Type extends InputType.Date
      ? string
      : Type extends InputType.Number
        ? number
        : never

/**
 * 将属性的联合类型转换为映射对象类型,例如:
 * 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: '作物',
      name: 'crop',
    },
    {
      input: InputType.Number,
      label: '产量(t)',
      name: 'yield_t',
    },
  ],
}

const initialData: Row<typeof config>[] = [
  { crop: '玉米', yield_t: 2 },
  { crop: '大麦', yield_t: 2 },
  // @ts-expect-error 这里应该报错,但实际没有
  { crop: 2, yield_t: '十' },
]

render(
  config,
  // @ts-expect-error 这里应该报错,但实际没有
  initialData,
)

问题现状

当前Row类型的字段值被推断为所有可能输入类型的联合类型(即string | number),而不是每个字段对应配置中指定的单一类型。

当前推断结果:

type Row<typeof config> = {
  crop: string | number;
  yield_t: string | number;
}

理想推断结果:

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

解决方案

问题出在TypeScript对config对象的类型推断上——默认情况下,TypeScript会将数组元素的类型推断为联合类型,而不是保留每个元素的字面量类型。我们需要通过const断言让TypeScript精确推断每个字段的类型,同时调整Config和Row的类型定义,确保能正确提取每个字段的输入类型。

修改后的完整代码

enum InputType {
  Text = 'Text',
  Number = 'Number',
  Date = 'Date',
}

// 调整FieldConfig为泛型,保留input的字面量类型
type FieldConfig<T extends InputType> = {
  input: T
  label: string
  name: string
}

// 让Config接受泛型,明确properties的元素类型为具体的FieldConfig
type Config<T extends readonly FieldConfig<any>[]> = {
  properties: T
}

// 重新定义Row类型,遍历每个属性并提取对应的输入类型
type Row<T extends readonly FieldConfig<any>[]> = {
  [K in T[number]['name']]: 
    Extract<T[number], { name: K }>['input'] extends InputType.Text 
      ? string 
      : Extract<T[number], { name: K }>['input'] extends InputType.Date 
        ? string 
        : Extract<T[number], { name: K }>['input'] extends InputType.Number 
          ? number 
          : never
}

// 调整render函数的泛型约束
const render = <T extends readonly FieldConfig<any>[]>(
  config: Config<T>, 
  initialData: Row<T>[]
) => {
  return initialData
}

// 使用as const断言让TypeScript精确推断每个字段的类型
const config = {
  properties: [
    {
      input: InputType.Text,
      label: '作物',
      name: 'crop',
    },
    {
      input: InputType.Number,
      label: '产量(t)',
      name: 'yield_t',
    },
  ],
} as const

const initialData: Row<typeof config.properties>[] = [
  { crop: '玉米', yield_t: 2 },
  { crop: '大麦', yield_t: 2 },
  // ✅ 此处正确触发TypeScript报错:类型"number"的参数不能赋给类型"string"的参数
  { crop: 2, yield_t: '十' },
]

render(
  config,
  // ✅ 此处正确触发TypeScript报错
  initialData,
)

关键修改点

  • const断言:在config对象后添加as const,让TypeScript保留每个input和name的字面量类型,而不是推断为InputType或string的宽泛类型。
  • 泛型调整:将Config和Row的泛型约束改为基于具体的FieldConfig数组,确保能准确提取每个字段的输入类型,而不是得到联合类型。
  • 类型映射优化:重新实现Row类型的映射逻辑,确保每个字段对应配置中指定的单一类型,而非所有可能类型的联合。

内容的提问来源于stack exchange,提问作者Good Idea

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 21:35:03