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
相关产品推荐
相关产品推荐

