React Hook Form泛型类型推断问题:如何按字段名约束值类型?
解决React Hook Form组件中根据name自动推断value类型的问题
问题描述
我基于react-hook-form定义了如下React组件类型:
import { type FieldPath, type FieldValues, type FieldPathValue, } from "react-hook-form"; interface FormControlRadioBoxProps< TFieldValues extends FieldValues, TFieldPath extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, > extends ComponentProps<typeof RadioBox> { name: TFieldPath; options: { name: string; value: FieldPathValue<TFieldValues, TFieldPath>; }[]; }
在组件使用时,顶层name属性有类型提示,但options[].value的类型并未根据指定的name(如'info.agreement')自动推断,而是解析为表单所有可能值的联合:
const TypedRadioBox = HookRadioBox<z.infer<typeof validator>>; // 其中info.agreement的类型为'yes'|'no' function ComponentInsideMyForm() { return <TypedRadioBox label='Do you agree?' name='info.agreement' options={[ { name: 'I do', value: 'yes', // 此处类型被解析为表单所有可能值的联合 }, { name: 'I do not', value: 'no', // 同样类型错误 }, ]} /> }
核心问题在于TFieldPath默认是所有可能路径的联合类型,没有被传入的具体name值窄化,导致value属性成为所有可能值的联合。
补充复现Demo
即使在纯TypeScript场景下,类似问题也会出现:
import type { FieldPath, FieldPathValue, FieldValues, } from "react-hook-form"; type FormData = { info: { name: string surname: string agreement: "yes" | "no" | "maybe" }, money: { income: number expenses: number } } interface FormControlInputProps< TFieldValues extends FieldValues, TFieldPath extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, > { name: TFieldPath; defaultValue: FieldPathValue<TFieldValues, TFieldPath>; } // 问题核心:defaultValue类型为FormData所有可能值的联合,未被name窄化 const dataDemo: FormControlInputProps<FormData> = { name: "info.agreement", defaultValue: "" // 此处本该报错,但实际类型校验失效 }
解决方案
通过将组件props定义为分布式联合类型,让TypeScript在匹配具体name时自动窄化对应的字段值类型:
1. 修改RadioBox组件的Props定义
import { type FieldPath, type FieldValues, type FieldPathValue, } from "react-hook-form"; import { RadioBox } from './your-radio-box-component'; // 定义辅助类型:将每个FieldPath映射为对应的Props分支 type FormControlRadioBoxProps<TFieldValues extends FieldValues> = { [TFieldPath in FieldPath<TFieldValues>]: { name: TFieldPath; options: { name: string; value: FieldPathValue<TFieldValues, TFieldPath>; }[]; } }[FieldPath<TFieldValues>] // 合并RadioBox的原有Props(排除name和options避免冲突) & Omit<ComponentProps<typeof RadioBox>, 'name' | 'options'>; // 组件定义为泛型函数,接收TFieldValues function HookRadioBox<TFieldValues extends FieldValues>( props: FormControlRadioBoxProps<TFieldValues> ) { // 组件逻辑实现 return <RadioBox {...props} />; }
2. 使用方式
import { z } from "zod"; // 假设你的表单校验Schema const validator = z.object({ info: z.object({ agreement: z.enum(['yes', 'no']) }) }); type FormData = z.infer<typeof validator>; function ComponentInsideMyForm() { return <HookRadioBox<FormData> label='Do you agree?' name='info.agreement' options={[ { name: 'I do', value: 'yes', // 类型正确推断为'yes'|'no' }, { name: 'I do not', value: 'no', // 类型正确 }, // 若传入其他值(如'unknown')会直接报错 ]} /> }
3. 针对复现Demo的修改
import type { FieldPath, FieldPathValue, FieldValues, } from "react-hook-form"; type FormData = { info: { name: string surname: string agreement: "yes" | "no" | "maybe" }, money: { income: number expenses: number } } // 修改为分布式联合类型 type FormControlInputProps<TFieldValues extends FieldValues> = { [TFieldPath in FieldPath<TFieldValues>]: { name: TFieldPath; defaultValue: FieldPathValue<TFieldValues, TFieldPath>; } }[FieldPath<TFieldValues>]; // 现在TypeScript会自动根据name推断defaultValue类型 const dataDemo: FormControlInputProps<FormData> = { name: "info.agreement", defaultValue: "yes" // 类型正确为"yes"|"no"|"maybe",传入""会触发类型错误 }
原理说明
原来的定义中,TFieldPath是所有FieldPath的联合类型,因此FieldPathValue<TFieldValues, TFieldPath>会被解析为所有字段值的联合。而通过将Props定义为每个FieldPath对应的独立Props分支的联合,TypeScript在匹配具体的name值时,会自动筛选出对应的分支,从而让value/defaultValue的类型与指定字段完全匹配。
内容的提问来源于stack exchange,提问作者Device
相关产品推荐
相关产品推荐

