React Hook Form + Zod联合Schema的TypeScript类型安全问题修复
解决React Hook Form结合Zod区分联合时错误类型缺失的问题
问题场景
我需要实现一个根据布尔值动态展示不同输入字段的表单,使用react-hook-form的useForm配合Zod的区分联合对象(分别对应布尔值两种状态的Zod对象)做输入校验。代码运行正常且错误提示能正常显示,但TypeScript中错误类型仅显示共享字段,缺失类型安全。
原代码:
import { zodResolver } from "@hookform/resolvers/zod" import React from "react" import { useForm } from "react-hook-form" import { z } from "zod" const TrueSchema = z.object({ isTrue: z.literal("true"), trueInput: z.string().min(1, "Required, true"), }) const FalseSchema = z.object({ isTrue: z.literal("false"), falseInput: z.string().min(1, "Required, false"), }) const FormSchema = z.discriminatedUnion("isTrue", [TrueSchema, FalseSchema]) type FormSchemaType = z.infer<typeof FormSchema> function Example() { const { register, watch, handleSubmit, formState: { errors }, } = useForm<FormSchemaType>({ resolver: zodResolver(FormSchema), defaultValues: { isTrue: "false", }, }) const onSubmit = handleSubmit((data) => console.log("data", data)) return ( <form onSubmit={onSubmit}> <label htmlFor="radioTrue"> <input type="radio" id="radioTrue" value="true" {...register("isTrue")} /> True </label> <label htmlFor="radioFalse"> <input type="radio" id="radioFalse" value="false" {...register("isTrue")} /> False </label> {watch("isTrue") === "true" && ( <> <input type="text" placeholder="Bool is true" {...register("trueInput")} /> {errors.trueInput && <p>{errors.trueInput.message}</p>} {/* 此处TypeScript仅识别errors.isTrue */} </> )} {watch("isTrue") === "false" && ( <> <input type="text" placeholder="Bool is false" {...register("falseInput")} /> {errors.falseInput && <p>{errors.falseInput.message}</p>} {/* 此处TypeScript仅识别errors.isTrue */} </> )} <input type="submit" /> </form> ) } export default Example
依赖版本:
"@hookform/resolvers": "^2.9.10", "react": "18.2.0", "react-dom": "18.2.0", "react-hook-form": "^7.38.0", "zod": "^3.19.1"
解决办法
核心思路是手动推导对应区分联合分支的错误类型,结合类型守卫或类型断言,让TypeScript能根据当前表单状态识别对应分支的错误字段。
方案1:自定义类型守卫(严谨性优先)
- 先定义每个分支的错误类型:
// 推导每个分支的格式化错误类型 type TrueErrors = z.inferFormattedError<typeof TrueSchema>; type FalseErrors = z.inferFormattedError<typeof FalseSchema>;
- 编写类型守卫函数:
// 判断是否为True分支的错误 function isTrueErrors(errors: unknown): errors is TrueErrors { return (errors as TrueErrors).isTrue?.value === "true"; } // 判断是否为False分支的错误 function isFalseErrors(errors: unknown): errors is FalseErrors { return (errors as FalseErrors).isTrue?.value === "false"; }
- 在组件中结合
watch结果和类型守卫使用:
修改渲染部分的错误判断逻辑:
{watch("isTrue") === "true" && ( <> <input type="text" placeholder="Bool is true" {...register("trueInput")} /> {isTrueErrors(errors) && errors.trueInput && <p>{errors.trueInput.message}</p>} </> )} {watch("isTrue") === "false" && ( <> <input type="text" placeholder="Bool is false" {...register("falseInput")} /> {isFalseErrors(errors) && errors.falseInput && <p>{errors.falseInput.message}</p>} </> )}
方案2:直接类型断言(简洁性优先)
如果不想写类型守卫,可以利用watch已经判断了isTrue值的前提,直接对errors做类型断言:
{watch("isTrue") === "true" && ( <> <input type="text" placeholder="Bool is true" {...register("trueInput")} /> {(errors as TrueErrors).trueInput && <p>{(errors as TrueErrors).trueInput.message}</p>} </> )} {watch("isTrue") === "false" && ( <> <input type="text" placeholder="Bool is false" {...register("falseInput")} /> {(errors as FalseErrors).falseInput && <p>{(errors as FalseErrors).falseInput.message}</p>} </> )}
完整修改后的代码
import { zodResolver } from "@hookform/resolvers/zod" import React from "react" import { useForm } from "react-hook-form" import { z } from "zod" const TrueSchema = z.object({ isTrue: z.literal("true"), trueInput: z.string().min(1, "Required, true"), }) const FalseSchema = z.object({ isTrue: z.literal("false"), falseInput: z.string().min(1, "Required, false"), }) const FormSchema = z.discriminatedUnion("isTrue", [TrueSchema, FalseSchema]) type FormSchemaType = z.infer<typeof FormSchema> // 推导分支错误类型 type TrueErrors = z.inferFormattedError<typeof TrueSchema>; type FalseErrors = z.inferFormattedError<typeof FalseSchema>; function Example() { const { register, watch, handleSubmit, formState: { errors }, } = useForm<FormSchemaType>({ resolver: zodResolver(FormSchema), defaultValues: { isTrue: "false", }, }) const onSubmit = handleSubmit((data) => console.log("data", data)) return ( <form onSubmit={onSubmit}> <label htmlFor="radioTrue"> <input type="radio" id="radioTrue" value="true" {...register("isTrue")} /> True </label> <label htmlFor="radioFalse"> <input type="radio" id="radioFalse" value="false" {...register("isTrue")} /> False </label> {watch("isTrue") === "true" && ( <> <input type="text" placeholder="Bool is true" {...register("trueInput")} /> {(errors as TrueErrors).trueInput && <p>{(errors as TrueErrors).trueInput.message}</p>} </> )} {watch("isTrue") === "false" && ( <> <input type="text" placeholder="Bool is false" {...register("falseInput")} /> {(errors as FalseErrors).falseInput && <p>{(errors as FalseErrors).falseInput.message}</p>} </> )} <input type="submit" /> </form> ) } export default Example
说明
两种方案都能让TypeScript正确识别对应分支的错误字段,恢复类型安全。方案1的类型守卫更严谨,适合复杂场景;方案2更简洁,适合简单场景。
内容的提问来源于stack exchange,提问作者Marvin
相关产品推荐
相关产品推荐

