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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 18:15:53