如何基于表单结构动态校验并推断提交数据类型?
TypeScript表单验证的类型推断问题解决
问题背景
在TypeScript中开发表单验证功能,表单由多个section组成,每个section对应特定答案类型(字符串、数字或数组)。目标是实现checkFormAnswers函数:接收表单与提交数据,先校验提交数据是否匹配表单预期结构;校验通过后,让TypeScript能正确推断提交数据的类型供后续逻辑使用。
当前遇到的问题:
- 已定义类型守卫
satisfiesForm,但直接访问submission['age']会报错 - 将
question断言为F[number]后,age被推断为string | number | string[],不符合预期 - 需要确认包含
BaseSection及子接口的复杂表单结构下,能否实现正确的类型推断
代码示例参考
基础结构示例
type Section = 'name' | 'age' | 'nicknames'; type Form = readonly Section[]; // 映射section到答案类型 type AnswerForSection<Section> = Section extends 'age' ? number : Section extends 'name' ? string : Section extends 'nicknames' ? string[] : never; // 预期提交结构 type ValidFormSubmission<F extends Form> = { [Section in F[number]]: AnswerForSection<Section> };
复杂结构示例
interface BaseSection { readonly id: string; } interface TextSection extends BaseSection { type: 'text'; } interface NumericSection extends BaseSection { type: 'numeric'; } interface MultipleChoiceSection extends BaseSection { type: 'multiple-choice'; } type Section = TextSection | NumericSection | MultipleChoiceSection; type Form = readonly Section[]; type AnswerForSection<S extends Section> = S['type'] extends 'numeric' ? number : S['type'] extends 'text' ? string : S['type'] extends 'multiple-choice' ? string[] : never; type AnswerForSections<Sections extends readonly Section[]> = { [K in Sections[number] as K['id']]: AnswerForSection<K> }; type ValidFormSubmission<F extends Form> = AnswerForSections<F>;
解决方案
核心思路
通过泛型约束+精确类型守卫,让TypeScript关联表单结构与提交数据的类型映射,避免宽泛的联合类型推断。关键是让类型守卫返回submission is ValidFormSubmission<F>,并确保表单参数为只读常量数组(用as const断言),让TypeScript追踪每个section的精确类型。
一、基础结构场景实现
type Section = 'name' | 'age' | 'nicknames'; type Form = readonly Section[]; type AnswerForSection<S extends Section> = S extends 'age' ? number : S extends 'name' ? string : S extends 'nicknames' ? string[] : never; type ValidFormSubmission<F extends Form> = { [K in F[number]]: AnswerForSection<K>; }; // 类型守卫:校验提交数据是否符合表单结构 function satisfiesForm<F extends Form>( form: F, submission: unknown ): submission is ValidFormSubmission<F> { if (typeof submission !== 'object' || submission === null) return false; const sub = submission as Record<string, unknown>; for (const section of form) { const value = sub[section]; switch (section) { case 'age': if (typeof value !== 'number') return false; break; case 'name': if (typeof value !== 'string') return false; break; case 'nicknames': if (!Array.isArray(value) || value.some(v => typeof v !== 'string')) return false; break; default: return false; } } // 可选:校验是否存在额外未知字段 return Object.keys(sub).every(key => form.includes(key as Section)); } // 验证函数:校验通过后自动推断提交数据类型 function checkFormAnswers<F extends Form>(form: F, submission: unknown) { if (satisfiesForm(form, submission)) { // 此处submission类型被精确推断 submission.age; // 类型为number(若表单包含'age') submission.name; // 类型为string(若表单包含'name') submission.nicknames; // 类型为string[](若表单包含'nicknames') } else { throw new Error('提交数据不符合表单要求'); } } // 使用示例 const testForm = ['name', 'age'] as const; checkFormAnswers(testForm, { name: 'Alice', age: 30 }); // 类型推断正确
二、复杂结构场景实现
针对包含BaseSection的复杂结构,利用映射类型的as子句提取section的id作为键,同时根据section的type字段做精确校验:
interface BaseSection { readonly id: string; } interface TextSection extends BaseSection { type: 'text'; } interface NumericSection extends BaseSection { type: 'numeric'; } interface MultipleChoiceSection extends BaseSection { type: 'multiple-choice'; } type Section = TextSection | NumericSection | MultipleChoiceSection; type Form = readonly Section[]; type AnswerForSection<S extends Section> = S['type'] extends 'numeric' ? number : S['type'] extends 'text' ? string : S['type'] extends 'multiple-choice' ? string[] : never; type AnswerForSections<Sections extends readonly Section[]> = { [K in Sections[number] as K['id']]: AnswerForSection<K>; }; type ValidFormSubmission<F extends Form> = AnswerForSections<F>; // 类型守卫 function satisfiesForm<F extends Form>( form: F, submission: unknown ): submission is ValidFormSubmission<F> { if (typeof submission !== 'object' || submission === null) return false; const sub = submission as Record<string, unknown>; for (const section of form) { const value = sub[section.id]; switch (section.type) { case 'numeric': if (typeof value !== 'number') return false; break; case 'text': if (typeof value !== 'string') return false; break; case 'multiple-choice': if (!Array.isArray(value) || value.some(v => typeof v !== 'string')) return false; break; default: return false; } } // 可选:校验额外字段 return Object.keys(sub).every(key => form.some(s => s.id === key)); } // 验证函数 function checkFormAnswers<F extends Form>(form: F, submission: unknown) { if (satisfiesForm(form, submission)) { // 类型精确匹配表单结构 submission['user-age']; // 若表单含id为'user-age'的NumericSection,类型为number submission['user-name']; // 若表单含id为'user-name'的TextSection,类型为string } else { throw new Error('提交数据不符合表单要求'); } } // 使用示例 const complexForm = [ { id: 'user-name', type: 'text' }, { id: 'user-age', type: 'numeric' } ] as const; checkFormAnswers(complexForm, { 'user-name': 'Bob', 'user-age': 25 }); // 类型推断正确
关键注意事项
- 表单参数必须用
as const断言为只读常量数组,让TypeScript能追踪每个section的精确类型,而非宽泛的联合类型。 - 类型守卫的返回值必须明确为
submission is ValidFormSubmission<F>,建立表单与提交数据的类型关联。 - 运行时校验逻辑要与类型定义完全匹配,确保类型推断的准确性。
内容的提问来源于stack exchange,提问作者GrahamDom
相关产品推荐
相关产品推荐

