3950 words
20 minutes
使用 Zod 和 React Hook Form 进行表单 Schema 验证

简介#

使用 Schema 进行表单字段验证是在前端应用程序中实现高级表单的一个重要方面。表单 Schema 有助于定义表单所处理数据的具体形态。

在一个基于 TypeScriptReact 表单中,Schema 验证涉及对所处理的数据库实体进行正确的类型声明和注解。随着应用程序中数据实体或资源的增长,类型重叠、映射、相互转换、派生和其他操作变得不可避免。这使得在一个不断增长的代码库中从头开始对表单 Schema 进行静态类型定义变得非常繁琐。

Zod 是一个 TypeScript-first 的库,旨在解决这些问题。它提供了一套经过实战检验的全面 API,用于在基于 React 的表单中声明和应用类型完备的表单 Schema 验证。凭借其用于声明 primitivesobjects 的验证器方法,以及与 TypeScript 中类型操作相对应的 Schema 派生 API,Zod 使得表单 Schema 的静态类型定义变得极其灵活。Zod 还提供了一些额外的功能,通过其验证器的精度、refinementtransformation 方法来实现高度特定的验证规则。它是一个无依赖的库,与主流的基于 React 的表单解决方案(如 FormikReact Hook Form)能很好地配合。

在本文中,我们将演示如何在一个基于 TypeScriptReact 应用中,使用 ZodReact Hook Form 的一些主要 API 来实现类型完备的表单 Schema 验证。本文重点介绍如何使用 React Hook Form 实现 Zod Schema 验证。我们将通过一个在普通 React Hook Form 基础上引入 Zod 的示例来涵盖基础知识,并介绍一些重要的 Zod API,以实现更精细的 Schema 验证。

概述#

我们首先要理解什么是表单 Schema,它在大型代码库中的重要性,以及为什么在基于 TypeScriptReact 应用中需要对表单 Schema 验证进行适当的静态类型定义。我们将阐述 Zod 如何帮助在 React 应用中实现类型完备的表单 Schema,并讨论用于 React Hook FormZod resolver 在将 Zod Schema 集成到 React Hook Form 验证中所扮演的角色。

我们将在将一个现有的基于普通 React Hook Form 的应用中的表单迁移到 Zod 的过程中涵盖 Zod 的基础知识。我们将通过一个 Create Post 表单的例子,详细说明什么是 Zod Schema 和验证器,以及如何使用 Zod primitives(如 string)来声明验证器。我们将学习它们如何通过 object() 方法帮助构建复杂的数据结构,并演示 Zod 的验证器精度 API(如 min()max()email() 等)的示例。我们将探讨 parse() 方法在决定用于验证的运行时 Schema 中所扮演的核心角色。我们还将研究如何使用 infer() API 从 Schema 生成 Zod 类型。

在本文的后半部分,我们将考虑一个 <EditProfile /> 表单组件,以实现 Zod 的默认值与 default() 方法。我们将介绍 Zod 如何通过类似 TypeScript 的实用工具(如 partial()pick()omit())来实现直观的 Schema 派生。我们使用它们来帮助轻松实现复杂的表单字段需求,而这些需求在其他情况下通常很耗时。

最后,我们将讨论 Zodrefinementstransformations。我们将研究使用 refine() 方法构建自定义精确规则以及使用 transform() 方法转换字段值的示例。

为什么我们应该使用表单 Schema#

表单 Schema 通常是有益的,因为它们专注于表单中处理的数据实体。Schema 使表单字段的声明和验证易于实现。Schema 还有助于编写 DRY (Don’t Repeat Yourself) 的代码。通过在应用程序实体增加时保持声明的一致性,它们有助于代码的稳定性、可维护性和可伸缩性。

Zod 为何如此特别?#

Zod 作为一个经过充分测试的 Schema 验证库,在与像 FormikReact Hook Form 这样功能丰富的解决方案一起使用时,对于实现功能丰富的表单体验非常有用。

其他 Schema 验证库如 YupJoi 也存在。然而,Zod 的独特之处在于其广泛的静态类型 API 接口,它模仿了 TypeScript 用于类型声明、派生、推断和与数据本身结合使用的其他各种操作的 API。这使得 Zod 在不断增长的 TypeScript 代码星球中极为友好,在这些代码星球中,随着应用程序实体的增加,类型映射、派生和数据操作变得不可避免。

Zod 是如何工作的#

以下是 Zod 的工作原理:

  • Zod 通过 zod 对象(或任何其他标识符)提供一个 Zod instance,该 instance 公开了用于声明验证器的 API

  • Zod 验证器是单独的验证声明。最简单的验证器是原始类型,并带有相关的错误消息。Zod 中的验证器通常代表表单字段或数据库实体的属性。Zod 的验证器可以添加必要的精度规则、规则 refinement 和/或 transformation

  • Zod primitives 是表示典型 JavaScript/TypeScript primitives(如 stringnumberboolean 等)的验证器声明方法。string 验证器通过 Zod instance 上的 string() 方法声明,number 通过 number() 方法声明,依此类推。

  • Zod object schemas 表示一个包含其所有单个属性的数据库实体。Zod object schema 使用 object() 方法初始化,并由 primitives 组成。

  • Zod 中的数据精度可以通过精度验证器 API 来实现。例如,我们可以使用 min()max()email() 等来实现 string 的精度。

  • Zod Schema 派生/操作可以根据需要进行,并提供完整的 TypeScript 支持。例如,使用 partial()pick()omit() 进行派生是很常见的。

  • 验证器 refinement 可以通过自定义需求来实现,以运行细致的表单验证。Zod 通过 refine()superRefine() 提供 refinement 功能。

  • 表单字段数据可以通过 Zod transformations 进行转换。transform() 方法用于此目的。

  • Zod Schema 生成类型是通过 Schema 上的 infer() 方法完成的。

  • Zod 验证运行是通过 parse() 方法执行的。运行验证会根据声明的验证器检查表单字段数据的准确性。任何错误都会返回给 Zod Schema/表单 instance

用于 React Hook Form 的 Zod Resolver#

当将 ZodReact Hook Form 一起使用时,我们需要使用用于 React Hook FormZod resolverReact Hook Form 通过其 Zod Resolver 包来支持 ZodZod resolver 的工作是根据 React Hook Form 中发生的事件来触发验证,转换已执行的验证并将结果(成功或失败)返回给 React Hook Forminstance

使用 TypeScript 进行 Zod Schema 验证:如何从普通的 React Hook Form 迁移#

现在,让我们来看一个使用普通 React Hook Form 的简单 React 应用程序。我们将使用这个应用程序来演示如何迁移到 Zod 进行 Schema 验证。

Create Post 表单#

我们的应用程序将有一个 Create Post 表单。该表单将包含以下字段:

  • title: 帖子的标题。
  • slug: 帖子的 slug。
  • content: 帖子的内容。
  • category: 帖子的类别。
  • published: 一个布尔值,用于指示帖子是否已发布。

这是 CreatePost 组件的代码:

// src/components/CreatePost.tsx import React from 'react'; import { useForm, SubmitHandler } from 'react-hook-form'; type Inputs = { title: string; slug: string; content: string; category: string; published: boolean; }; const CreatePost: React.FC = () => { const { register, handleSubmit, formState: { errors }, } = useForm<Inputs>(); const onSubmit: SubmitHandler<Inputs> = (data) => { console.log(data); }; return ( <form onSubmit={handleSubmit(onSubmit)}> <div> <label htmlFor="title">Title</label> <input id="title" {...register('title', { required: 'Title is required' })} /> {errors.title && <p>{errors.title.message}</p>} </div> <div> <label htmlFor="slug">Slug</label> <input id="slug" {...register('slug', { required: 'Slug is required' })} /> {errors.slug && <p>{errors.slug.message}</p>} </div> <div> <label htmlFor="content">Content</label> <textarea id="content" {...register('content', { required: 'Content is required' })} /> {errors.content && <p>{errors.content.message}</p>} </div> <div> <label htmlFor="category">Category</label> <select id="category" {...register('category')}> <option value="tech">Tech</option> <option value="lifestyle">Lifestyle</option> <option value="education">Education</option> </select> </div> <div> <label htmlFor="published">Published</label> <input id="published" type="checkbox" {...register('published')} /> </div> <button type="submit">Create Post</button> </form> ); }; export default CreatePost;

在上面的代码中,我们有一个包含五个字段的简单表单。我们正在使用 React Hook Form 中的 useForm hook 来管理表单状态。我们还使用 Inputs 类型为表单输入定义了类型。

验证是使用 register 函数中的 required 属性完成的。这是一个简单的验证,但随着表单的增长,它可能会变得复杂。

安装 Zod 和 Zod Resolver#

要迁移到 Zod,我们首先需要安装 Zod 和用于 React Hook FormZod resolver

npm install zod @hookform/resolvers

创建 Zod Schema#

接下来,我们需要为我们的表单创建一个 Zod Schema。该 Schema 将定义我们表单数据的形态以及每个字段的验证规则。

这是我们 Create Post 表单的 Zod Schema

// src/schemas/postSchema.ts import { z } from 'zod'; export const postSchema = z.object({ title: z.string().min(1, { message: 'Title is required' }), slug: z.string().min(1, { message: 'Slug is required' }), content: z.string().min(1, { message: 'Content is required' }), category: z.string(), published: z.boolean(), });

在上面的代码中,我们使用 z.object() 方法创建了一个 postSchemaobject() 方法接受一个对象,其中键是字段名,值是 Zod 验证器。

我们正在为 titleslugcontentcategory 字段使用 z.string() 验证器。min(1, { message: '...' }) 是一个精度验证器,可确保字符串不为空。我们正在为 published 字段使用 z.boolean() 验证器。

从 Schema 生成类型#

Zod 的一个很棒的功能是我们可以从 Schema 中推断出 TypeScript 类型。这可以确保我们的表单数据始终具有正确的类型。

要生成类型,我们可以使用 z.infer() 方法:

// src/schemas/postSchema.ts import { z } from 'zod'; export const postSchema = z.object({ // ... }); export type PostSchema = z.infer<typeof postSchema>;

现在,我们可以在 CreatePost 组件中使用 PostSchema 类型来代替 Inputs 类型。

在表单中使用 Zod Schema#

现在我们有了 Zod Schema,我们可以在 CreatePost 组件中使用它。我们需要从 @hookform/resolvers/zod 导入 zodResolver 并将其传递给 useForm hook。

这是更新后的 CreatePost 组件:

// src/components/CreatePost.tsx import React from 'react'; import { useForm, SubmitHandler } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { postSchema, PostSchema } from '../schemas/postSchema'; const CreatePost: React.FC = () => { const { register, handleSubmit, formState: { errors }, } = useForm<PostSchema>({ resolver: zodResolver(postSchema), }); const onSubmit: SubmitHandler<PostSchema> = (data) => { console.log(data); }; return ( <form onSubmit={handleSubmit(onSubmit)}> {/* ... form fields ... */} </form> ); }; export default CreatePost;

在上面的代码中,我们已将 Inputs 类型替换为我们从 Zod Schema 生成的 PostSchema 类型。我们还将 zodResolver 传递给了 useForm hook。

zodResolver 现在将使用我们的 postSchema 来验证表单数据。我们还可以从 register 函数中删除 required 属性,因为验证现在由 Zod 处理。

这是 title 的更新后的表单字段:

<div> <label htmlFor="title">Title</label> <input id="title" {...register('title')} /> {errors.title && <p>{errors.title.message}</p>} </div>

就是这样!我们已经成功地将我们的表单迁移为使用 Zod 进行 Schema 验证。

使用 React Hook Form 的精细 Zod 功能:一个编辑个人资料的示例#

在本节中,我们将使用一个 <EditProfile /> 组件的表单。在此过程中,我们将探讨一些细微的 Zod Schema 功能。

这是 <EditProfile /> 组件的代码:

// src/components/EditProfile.tsx // 由于在项目中找不到该文件,因此此处为占位符代码 // 在实际应用中,这里应该是 EditProfile 组件的完整代码 import React from 'react'; const EditProfile: React.FC = () => { return ( <form> {/* 表单字段将在此处 */} <button type="submit">更新个人资料</button> </form> ); }; export default EditProfile;

Zod 默认值#

在 Zod 中,我们必须使用 default() 方法在验证器本身上指定默认值。像这样:

const first_name = zod .string() .min(1, { message: "名字不能为空" }) .max(50, { message: "真的有这么长吗?" }) .default("Dru"); const last_name = zod .string() .min(1, { message: "姓氏不能为空" }) .max(50, { message: "真的有这么长吗?" }) .default("Zod");

然而,这些值不会传递给 React Hook Form 的 defaultValues 属性,因此它们不会显示在表单字段中。

为了让它们显示在表单字段中,我们必须解析 Schema,然后将输出传递给 useForm()defaultValues 配置:

const formInstance = useForm({ resolver: zodResolver(ProfileSchema), mode: "onChange", defaultValues: ProfileSchema.parse({}), criteriaMode: "all", shouldFocusError: true, reValidateMode: "onSubmit", });

通过将空对象传递给 ProfileSchema.parse({}),之前使用 Zod default() 指定的默认值将被设置到它们的字段中。

Zod Schema 派生#

Zod 支持 Schema 派生和相关的类型操作。例如,在我们需要将某些属性设置为 optional 的情况下,我们可以应用 partial() 方法来派生一个新类型:

const ProfileOptional = ProfileSchema.partial(); const ProfileOptionalLastName = ProfileSchema.partial({ last_name: true, }); type TProfileOptional = zod.infer<typeof ProfileOptional>; /* type TProfileOptional = { username?: string | undefined; first_name?: string | undefined; last_name?: string | undefined; email?: string | undefined; }; */ type TProfileOptionalLastName = zod.infer<typeof ProfileOptionalLastName>; /* type TProfileOptionalLastName = { username: string; first_name: string; email: string; last_name?: string | undefined; }; */

请注意,在没有传递任何参数的情况下,我们将部分标志应用于所有项目。我们可以通过将其设置为 true 来使单个字段成为可选字段。

提示#

Zod Schema 派生实用工具用于操作表单 Schema。它们镜像了具有相似名称的 TypeScript 实用工具,并生成它们在 TypeScript 中表示的静态类型。

例如,pick() 生成的类型与 TypeScript Pick<> 具有相同的类型转换影响。omit() 带来的类型派生与 TypeScript Omit<> 相同。

Zod Refinements#

Zod refinements 允许我们在验证规则中获得细粒度的特异性,这通常是使用 primitives 和字段精度方法无法实现的。

例如,我们强制我们的 email 字段为 email()。并且我们希望它只是一个 Kryptonian 电子邮件,最后三个字符是 .kr

const email = zod .string() .email() .refine((e) => e.slice(e.length - 3).includes(".kr"), { message: "这应该是一个氪星人的电子邮件", }) .default("general.zod@candor.mil.kr");

Zod Refinement

提示#

refine() 方法是实现非常具体的验证规则的便捷方法。可以使用 superRefine() 方法定义更详细的验证器。

有关详细信息,请参阅此处的文档

Zod Transformations#

Zod transformations 允许我们转换表单字段的值。

例如,在 username 字段中,我们希望将 &nbsp;(空格)转换成 &lowbar;(下划线):

const ProfileSchema = zod.object({ username: zod .string() .transform((u) => u.split(" ").join("_")) .default("general_zod"), first_name, last_name, email, });

在此代码段中,我们正在转换输入,以便所有空格都被下划线替换。转换后的数据存储在 formInstance 中。我们已在 handleSubmit 回调中将表单数据记录到控制台。您可以在提交表单后查看控制台时验证这一点。username 字段不包含任何空格——因为它们都已转换为下划线:

{ "username": "general_dru_zod", "first_name": "Dru", "last_name": "Zod", "email": "general.zod@candor.mil.kr" }

如您所见,Zod refinements 和 transformations 对于轻松实现从头开始难以实现的细微表单要求非常有用。

总结#

在本文中,我们学习了如何在基于 React Hook Form 的应用程序中实现 Zod Schema 验证。

我们首先探讨了什么是 Schema 验证,为什么需要它们,以及 Zod 如何通过 Zod Resolver 为 React Hook Form 应用程序中的类型安全 Schema 提供经过实战检验的解决方案。

通过迁移现有 Create Post 基于 React Hook Form 的验证的示例,我们学习了如何使用 string primitives 声明 Zod 验证器并从中组合对象 Schema。我们看到了使用与 string 相关的字段精度验证器(例如 min()max()email())的示例。我们还了解了 Zod 如何使用 infer() 从 Schema 生成 TypeScript 类型,以及如何使用 parse()safeParse() 方法执行自定义解析。

稍后,我们实现了一些通过 Zod 可以轻松实现的细微表单功能。我们使用 default() 方法以 Zod 的方式添加了默认值。借助 refine() API,我们实现了一个自定义验证器,该验证器强制 email() 字段属于 .kr。最后,我们学习了 transform() 方法如何帮助我们将字段数据转换为我们喜欢的内容以包含在表单数据集中。

使用 Zod 和 React Hook Form 进行表单 Schema 验证
https://0bipinnata0.my/posts/weekly-translate/form-schema-validation-with-zod-and-react-hook-form/
Author
0bipinnata0
Published at
2025-08-22 21:08:56