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

使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 10:34:53