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

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:自定义类型守卫(严谨性优先)

  1. 先定义每个分支的错误类型:
// 推导每个分支的格式化错误类型
type TrueErrors = z.inferFormattedError<typeof TrueSchema>;
type FalseErrors = z.inferFormattedError<typeof FalseSchema>;
  1. 编写类型守卫函数:
// 判断是否为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";
}
  1. 在组件中结合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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.15 15:45:42