使用react-hook-form与Zod时默认值的TypeScript错误解决
问题描述
我正在使用react-hook-form、Zod和Shadcn/ui构建表单,其中shippingPrice字段需要验证输入为美元价格。当前设置下,将该字段默认值设为空字符串时出现TypeScript错误,原因是我将输入值强制转换为数字。需要在不破坏验证逻辑、也不使用默认值为“0”的数字输入框的前提下修复这个错误。
相关代码如下:
const FormSchema = z.object({ title: z.string().min(10, { message: "Item title must be at least 10 characters.", }), freeShipping: z.boolean(), shippingPrice: z.coerce .number({ required_error: "Please enter a shipping price.", invalid_type_error: "Shipping price must contain only numbers and decimals.", }) .min(1, "Minimum shipping price is $1.") .max(1000, "Maximum buy now price is $1,000.") .multipleOf(0.01, { message: "Buy now price must have no more than 2 decimal places.", }), }); export default function ShippingStep({ updateListing, }: { updateListing: Function; }) { const form = useForm<z.infer<typeof FormSchema>>({ resolver: zodResolver(FormSchema), defaultValues: { title: "", shippingPrice: "", // TypeScript错误:Type 'string' is not assignable to type 'number'. freeShipping: false, }, }); // ... 其余组件代码 }
补充:Shadcn/ui的Input组件为默认生成代码:
import * as React from "react" import { cn } from "@/lib/utils" export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {} const Input = React.forwardRef<HTMLInputElement, InputProps>( ({ className, type, ...props }, ref) => { return ( <input type={type} className={cn( "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", className )} ref={ref} {...props} /> ) } ) Input.displayName = "Input" export { Input }
解决方案
针对这个场景,有两种核心修复思路,都能满足你的要求:
方案1:调整Zod Schema,兼容空字符串并添加条件验证
通过Zod的union和refine,让shippingPrice支持空字符串输入,同时保证仅当未勾选免运费时该字段必填,最后转换为数字类型。
修改后的Schema代码:
const FormSchema = z.object({ title: z.string().min(10, { message: "Item title must be at least 10 characters.", }), freeShipping: z.boolean(), shippingPrice: z.union([ // 允许空字符串输入 z.string().empty(), // 保留原有的数字验证规则 z.coerce.number({ invalid_type_error: "Shipping price must contain only numbers and decimals.", }) .min(1, "Minimum shipping price is $1.") .max(1000, "Maximum shipping price is $1,000.") .multipleOf(0.01, { message: "Shipping price must have no more than 2 decimal places.", }) ]) // 验证:未勾选免运费时,运费不能为空字符串 .refine(val => val !== "", { message: "Please enter a shipping price.", }) // 将空字符串转换为undefined(或根据需求转成其他值),最终输出数字类型 .transform(val => val === "" ? undefined : Number(val)), }) // 全局条件验证:确保未勾选免运费时,shippingPrice存在且有效 .refine(data => { if (!data.freeShipping && !data.shippingPrice) { return false; } return true; }, { message: "Shipping price is required when free shipping is not selected.", path: ["shippingPrice"], // 错误提示精准定位到该字段 });
对应的useForm配置:
const form = useForm<z.infer<typeof FormSchema>>({ resolver: zodResolver(FormSchema), defaultValues: { title: "", shippingPrice: "", // 现在符合Schema的类型要求,TS不再报错 freeShipping: false, }, });
方案2:修改react-hook-form字段处理逻辑,适配文本输入与数字类型
保持原有Zod Schema核心规则不变,通过调整FormField的输入值处理,让文本输入框的空字符串与表单内部的数字类型兼容。
首先,调整Zod Schema和useForm默认值:
const FormSchema = z.object({ // ... 原有title、freeShipping字段 shippingPrice: z.coerce .number({ required_error: "Please enter a shipping price.", invalid_type_error: "Shipping price must contain only numbers and decimals.", }) .min(1, "Minimum shipping price is $1.") .max(1000, "Maximum shipping price is $1,000.") .multipleOf(0.01, { message: "Shipping price must have no more than 2 decimal places.", }) .optional(), // 添加optional()允许undefined值 }).refine(data => { // 全局条件验证:未勾选免运费时必须填写运费 if (!data.freeShipping && !data.shippingPrice) { return false; } return true; }, { message: "Shipping price is required when free shipping is not selected.", path: ["shippingPrice"], }); // useForm配置 const form = useForm<z.infer<typeof FormSchema>>({ resolver: zodResolver(FormSchema), defaultValues: { title: "", shippingPrice: undefined, freeShipping: false, }, });
然后修改FormField的渲染逻辑,适配文本输入:
<FormField control={form.control} name="shippingPrice" render={({ field }) => ( <FormItem> <FormLabel>Shipping Charge</FormLabel> <FormControl> <Input className="w-full max-w-xs" placeholder="Shipping charge" // 将数字值转为字符串显示,空值时显示空字符串 value={typeof field.value === 'number' ? field.value.toString() : ''} // 输入空字符串时传递undefined给表单,否则转为数字 onChange={(e) => { const inputVal = e.target.value; field.onChange(inputVal === "" ? undefined : Number(inputVal)); }} /> </FormControl> <FormMessage /> </FormItem> )} />
内容的提问来源于stack exchange,提问作者Max
相关产品推荐
相关产品推荐

