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
相关产品推荐
相关产品推荐

