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

如何用TypeScript为react-hook-form无默认值场景正确类型标注

解决react-hook-form + zod + react-query栈中表单值的TypeScript类型标注问题

在使用react-hook-form、zod和react-query的技术栈时,若表单未提供必填字段的默认值,且初始值来自异步的react-query请求,TypeScript会误判表单值的类型(比如将可能为undefined的字段标记为string),导致运行时调用方法(如split)时抛出错误。

问题根源

react-query的data初始状态为undefined,而useForm在指定泛型为zod推断的FormValues(所有字段必填)时,TypeScript会忽略初始值可能缺失的情况,导致类型误判。

解决方案

方法一:区分加载阶段与完成阶段的表单类型

通过定义Partial类型描述加载阶段的表单状态,数据加载完成后再重置为完整的必填类型:

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useQuery } from "@tanstack/react-query";
import { useEffect } from "react";

// 定义zod校验schema
const schema = z.object({
  name: z.string(),
});

// 完整的表单值类型(提交时的类型)
type FormValues = z.infer<typeof schema>;
// 加载阶段的表单类型(所有字段可选)
type PartialFormValues = Partial<FormValues>;

function App() {
  const { data: formValuesFromApi, isSuccess } = useQuery({
    queryKey: ["query key"],
    queryFn: async () => {
      await new Promise(resolve => setTimeout(resolve, 1000));
      return { name: "John" };
    },
  });

  // 初始化表单时使用Partial类型,明确字段可能为undefined
  const methods = useForm<PartialFormValues>({
    resolver: zodResolver(schema),
    defaultValues: {},
  });

  // 数据加载成功后,重置表单为完整的必填值
  useEffect(() => {
    if (isSuccess && formValuesFromApi) {
      methods.reset(formValuesFromApi);
    }
  }, [isSuccess, formValuesFromApi, methods]);

  // 提交时zod会校验数据为完整的FormValues类型
  const onSubmit = (data: FormValues) => {
    console.log(data);
  };

  const formValues = methods.getValues();

  // TypeScript会正确提示name可能为undefined,需先做判断
  if (formValues.name) {
    const nameChars = formValues.name.split("");
    console.log("nameChars", nameChars);
  }

  return (
    <form onSubmit={methods.handleSubmit(onSubmit)}>
      <input {...methods.register("name")} />
      <input type="submit" />
    </form>
  );
}

export default App;

方法二:严格空值检查+类型守卫

如果不想修改表单的泛型类型,可以在访问表单值时添加严格的空值检查,结合TypeScript的类型守卫:

const formValues = methods.getValues();

// 类型守卫确保name存在后再调用方法
if (typeof formValues.name === "string") {
  const nameChars = formValues.name.split("");
  console.log("nameChars", nameChars);
}

// 或者使用可选链+默认值
const nameChars = formValues.name?.split("") || [];

这种方法适合简单场景,但无法从根源上让TypeScript感知初始状态的类型风险。

方法三:修改zod schema适配初始状态

如果允许字段在初始时为undefined,但提交时必须非空,可以调整zod schema为可选字段,再通过required()确保提交时的校验:

const schema = z.object({
  name: z.string().optional().required("Name is required"),
});

type FormValues = z.infer<typeof schema>; // 此时FormValues的name为string | undefined

但这种方式会让FormValues包含可选字段,需要在提交逻辑中额外处理,适合对表单初始化逻辑要求不高的场景。

核心思路

让TypeScript明确感知表单值在不同阶段的类型差异:加载阶段字段可能为undefined,数据加载完成或提交时字段为必填类型。通过Partial类型、条件重置表单或严格空值检查,避免类型误判导致的运行时错误。

内容的提问来源于stack exchange,提问作者Anton Arbus

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 02:37:35