如何用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
相关产品推荐
相关产品推荐

