React FormBuilder组件:修复Path<FieldValues>类型匹配错误
问题描述
我正在构建FormBuilder组件,相关代码如下:
type FormValues = { [i: number]: { [j: number]: string; }; }; export type FormBuilderProps<> = { schema: Array<{ legend: string; fields: Array<TF | SF>; }>; onSubmit: SubmitHandler<FormValues>; }; export function FormBuilder(props: FormBuilderProps) { const { schema, onSubmit } = props; const { handleSubmit, control } = useForm({ resolver: useMemo(() => { return zodResolver( z.object( schema.reduce((result, { fields }, i) => { return Object.assign({}, result, { [i]: fields.reduce((rules, { schema }, j) => { if (schema) { return Object.assign({}, rules, { [j]: schema, }); } return rules; }, {}), }); }, {}), ), ); }, [schema]), }); return ( <form onSubmit={handleSubmit((data) => { return onSubmit(data); })} > {schema.map((fieldset, i) => { const { fields } = fieldset; return ( <fieldset key={`fieldset-${i}`}> {fields.map((field, j) => { const name = `${i}.${j}`; return ( <Controller control={control} key={`field-${j}`} name={name} render={(renderProps) => { if (field.kind === "text") { const { props } = field; return <input {...renderProps.field} {...props} />; } if (field.kind === "select") { const { props } = field; return <input {...renderProps.field} {...props} />; } throw new Error(`Unsupported kind of field`); }} /> ); })} </fieldset> ); })} <input type='submit' /> </form> ); }
Controller组件的name属性类型应为Path<FieldValues>,但我不清楚如何定义该类型以匹配schema字段的类型,因此出现以下错误:
Type 'string' is not assignable to type 'Path<TFieldValues>' controller.d.ts(20, 5): The expected type comes from property 'name' which is declared here on type 'IntrinsicAttributes & { render: ({ field, fieldState, formState, }: { field: ControllerRenderProps<TFieldValues, Path<TFieldValues>>; fieldState: ControllerFieldState; formState: UseFormStateReturn<...>; }) => ReactElement<...>; } & UseControllerProps<...>'
当前我的FormValues类型过于抽象,这应该是导致错误的原因。请问应如何定义FormValues类型以解决该错误?
解决方案
要解决类型不匹配问题,核心是让FormValues与传入的schema强关联,通过泛型推导生成具体类型,替代抽象的索引类型。以下是具体修改步骤:
1. 明确字段基础类型
先定义TextField和SelectField的具体类型,包含kind、props和Zod校验规则:
import { z } from "zod"; import { SubmitHandler, useForm, Controller, Path, zodResolver } from "react-hook-form"; // 文本字段类型 type TextField<T extends z.ZodTypeAny = z.ZodString> = { kind: "text"; props: React.InputHTMLAttributes<HTMLInputElement>; schema: T; }; // 选择字段类型 type SelectField<T extends z.ZodTypeAny = z.ZodString> = { kind: "select"; props: React.SelectHTMLAttributes<HTMLSelectElement>; schema: T; }; type FormField = TextField | SelectField;
2. 添加泛型约束并推导FormValues
修改FormBuilderProps,用泛型接收具体schema类型,再从schema推导对应的FormValues:
// 从Fieldset数组推导FormValues类型 type FormValuesFromSchema<Schema extends Array<{ fields: FormField[] }>> = { [K in keyof Schema]: { [J in keyof Schema[K]["fields"]]: Schema[K]["fields"][J]["schema"] extends z.ZodType<infer T> ? T : never; }; }; export type FormBuilderProps<Schema extends Array<{ legend: string; fields: FormField[] }>> = { schema: Schema; onSubmit: SubmitHandler<FormValuesFromSchema<Schema>>; };
3. 调整组件内部类型推导
在FormBuilder组件中使用泛型参数,让useForm使用推导后的FormValues类型:
export function FormBuilder<Schema extends Array<{ legend: string; fields: FormField[] }>>(props: FormBuilderProps<Schema>) { const { schema, onSubmit } = props; type FormValues = FormValuesFromSchema<Schema>; const { handleSubmit, control } = useForm<FormValues>({ resolver: zodResolver( z.object( schema.reduce((result, { fields }, i) => { return { ...result, [i]: z.object( fields.reduce((rules, { schema }, j) => { return { ...rules, [j]: schema }; }, {}) ) }; }, {}) as z.ZodObject<{ [K in keyof FormValues]: z.ZodObject<FormValues[K]> }> ) ), }); return ( <form onSubmit={handleSubmit(onSubmit)}> {schema.map((fieldset, i) => { const { legend, fields } = fieldset; return ( <fieldset key={`fieldset-${i}`}> <legend>{legend}</legend> {fields.map((field, j) => { // 断言为Path<FormValues>,因为i/j是数组索引,对应有效路径 const name = `${i}.${j}` as Path<FormValues>; return ( <Controller control={control} key={`field-${j}`} name={name} render={(renderProps) => { if (field.kind === "text") { return <input {...renderProps.field} {...field.props} />; } if (field.kind === "select") { return <select {...renderProps.field} {...field.props} />; } throw new Error(`Unsupported kind of field: ${(field as any).kind}`); }} /> ); })} </fieldset> ); })} <input type='submit' /> </form> ); }
关键说明
FormValuesFromSchema泛型会根据传入的schema数组,生成嵌套对象类型,每个字段类型由其Zod规则推导而来。- 在
useForm中,通过类型断言确保Zod resolver的类型与FormValues匹配。 name =${i}.${j}as Path<FormValues>:数组索引对应的路径字符串必然是FormValues的有效路径,此类型断言安全且能解决类型不匹配错误。
使用示例
现在使用FormBuilder时,类型会自动推导:
const schema = [ { legend: "个人信息", fields: [ { kind: "text", props: { placeholder: "姓名" }, schema: z.string().min(2) }, { kind: "select", props: { children: <> <option value="male">男</option> <option value="female">女</option> </> }, schema: z.enum(["male", "female"]) } ] } ]; <FormBuilder schema={schema} onSubmit={(data) => { // data类型自动推导为:{ 0: { 0: string; 1: "male" | "female" } } console.log(data); }} />
内容的提问来源于stack exchange,提问作者Pavel Perevezencev
相关产品推荐
相关产品推荐

