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

TypeScript中Zod泛型扩展类型在子组件的使用问题

问题:基于Zod和React Hook Form的通用表单组件类型错误

我定义了表单基础接口IProductSourceBase,各页面会扩展该接口生成专属表单。现在要创建一个子组件,基于扩展后的模板生成表单,但遇到了类型错误,同时表单元素无法识别字段名称。

最小复现代码

// childComponent.tsx  
export default function ProductSourceFormTemplate<T extends IProductSourceBase>({ formSchema }: {
    formSchema: z.ZodType<T>,
}) {  
    type FormValues = z.infer<typeof formSchema>
    const defaultValues: T = useMemo<T>(() => ({
        sourceId: ""
    }) as T, [searchParams])
    const form = useForm<FormValues>({
        resolver: zodResolver(formSchema),
        defaultValues: defaultValues,
    });

    // 渲染表单
}

错误信息

Type 'T' is not assignable to type 'AsyncDefaultValues<T> | DefaultValues<T> | undefined'.
  Type 'IProductSourceBase' is not assignable to type 'AsyncDefaultValues<T> | DefaultValues<T> | undefined'.
    Type 'IProductSourceBase' is not assignable to type 'DefaultValues<T>'.ts(2322)

需求

父组件通过productBaseSchema.extend({})生成扩展后的z.ZodObject,传递给子组件;子组件需支持接收onSubmit、渲染函数等props,额外字段仅由父组件处理。

基础示例代码

export interface IProductSourceBase {
    productId: string;
    sourceId: number;
    sourceMeta?: any;
}

const productSourceBaseSchema = z.object({
    productId: z.string().nonempty(translate("Product ID is required")),
    sourceId: z.coerce.number().int(translate("Source ID must be an integer")).gt(0, translate("A source must be selected")),
});

const extendedSchema = productSourceBaseSchema.extend({
    sourceMeta: z.object({
        sourceUrl: z.string().url(translate("This is not a valid URL")).nonempty(translate("Url is required")).refine(
            (url) => /\/spreadsheets\/d\/([a-zA-Z0-9-_]{16,44})/.test(url), // 验证Spreadsheet ID格式
            {
                message: translate("This is not a valid Google Spreadsheet ID"), // 自定义错误提示
            }
        ),
        cell: z.string().nonempty(translate("Cell number is required")).refine(
            (cell) => /^[A-Za-z]+[1-9]\d*$/.test(cell), // 验证单元格格式
            {
                message: translate("Invalid cell reference format"), // 自定义错误提示
            }
        ),
    }),
});

解决方案

1. 修复类型错误与组件重构

问题核心在于泛型约束过宽、默认值结构不匹配,以及字段渲染的耦合问题。以下是优化后的子组件:

// childComponent.tsx
import { useForm, DefaultValues } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z, ZodObject } from "zod";
import { IProductSourceBase } from "./your-path";

// 精确约束props类型
type ProductSourceFormProps<T extends IProductSourceBase> = {
  formSchema: ZodObject<z.infer<typeof productSourceBaseSchema> & z.ZodRawShape>;
  defaultValues?: DefaultValues<T>;
  onSubmit: (data: T) => void;
  renderExtraFields: (form: ReturnType<typeof useForm<T>>) => React.ReactNode;
};

export default function ProductSourceFormTemplate<T extends IProductSourceBase>({
  formSchema,
  defaultValues = {} as DefaultValues<T>,
  onSubmit,
  renderExtraFields,
}: ProductSourceFormProps<T>) {
  type FormValues = z.infer<typeof formSchema>;

  const form = useForm<FormValues>({
    resolver: zodResolver(formSchema),
    defaultValues,
  });

  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      {/* 渲染基础公共字段 */}
      <div>
        <label>Product ID</label>
        <input {...form.register("productId")} />
        {form.formState.errors.productId && <p>{form.formState.errors.productId.message}</p>}
      </div>
      <div>
        <label>Source ID</label>
        <input type="number" {...form.register("sourceId")} />
        {form.formState.errors.sourceId && <p>{form.formState.errors.sourceId.message}</p>}
      </div>

      {/* 父组件自定义的额外字段 */}
      {renderExtraFields(form)}

      <button type="submit">提交</button>
    </form>
  );
}

2. 父组件使用示例

// ParentComponent.tsx
import ProductSourceFormTemplate from "./ProductSourceFormTemplate";
import { extendedSchema } from "./your-path";

export default function ParentComponent() {
  const handleSubmit = (data: z.infer<typeof extendedSchema>) => {
    console.log("提交数据:", data);
  };

  return (
    <ProductSourceFormTemplate
      formSchema={extendedSchema}
      defaultValues={{
        productId: "",
        sourceId: 0,
        sourceMeta: {
          sourceUrl: "",
          cell: "",
        },
      }}
      onSubmit={handleSubmit}
      renderExtraFields={(form) => (
        <>
          <div>
            <label>数据源URL</label>
            <input {...form.register("sourceMeta.sourceUrl")} />
            {form.formState.errors.sourceMeta?.sourceUrl && <p>{form.formState.errors.sourceMeta.sourceUrl.message}</p>}
          </div>
          <div>
            <label>单元格位置</label>
            <input {...form.register("sourceMeta.cell")} />
            {form.formState.errors.sourceMeta?.cell && <p>{form.formState.errors.sourceMeta.cell.message}</p>}
          </div>
        </>
      )}
    />
  );
}

3. 关键优化点

  • 精确泛型约束:将formSchema限定为基于基础schema扩展的ZodObject,避免泛型过于宽泛导致的类型推断失效。
  • 默认值解耦:子组件不再硬编码默认值,由父组件传递符合扩展后类型的默认值,保证类型安全。
  • 渲染职责分离:通过renderExtraFields让父组件完全控制额外字段的渲染逻辑,子组件仅负责公共字段,实现高复用性。
  • 移除不安全断言:去掉子组件中as T的强制类型断言,改用类型推导保证类型匹配。

内容的提问来源于stack exchange,提问作者AMunim

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 20:12:41