Zod 常用案例总结

Zod 常用案例总结

官方文档:https://zod.dev/

一、先懂核心(零基础必看)

1. Zod 是什么?

Zod 是 TS 优先的运行时数据校验库,核心解决两个问题:

  • 校验运行时数据(如表单、接口返回)是否符合预期;
  • 从校验规则自动推导 TS 类型,无需手动写 interface。

2. 核心概念(3个关键词)

概念 说明
Schema 校验规则(比如 z.string() 是字符串校验规则)
safeParse 安全校验方法(失败不抛异常,返回结果对象)
z.infer 从 Schema 自动推导 TS 类型的工具函数

3. 前置准备(1步搞定)

bash 复制代码
# 安装 Zod(仅需这1行命令)
npm install zod

二、基础入门(从零开始写)

场景1:单个基础类型校验(新手第一步)

目标

学会定义最简单的校验规则,用 safeParse 校验数据

代码(可直接复制)
typescript 复制代码
import { z } from "zod";

// 1. 定义 Schema(校验规则)
const strSchema = z.string(); // 校验字符串
const numSchema = z.number(); // 校验数字
const boolSchema = z.boolean(); // 校验布尔值

// 2. 用 safeParse 校验数据(核心方法)
// 校验成功
const successResult = strSchema.safeParse("hello zod");
console.log(successResult.success); // true
console.log(successResult.data); // "hello zod"

// 校验失败
const failResult = strSchema.safeParse(123); // 传入数字,校验失败
console.log(failResult.success); // false
console.log(failResult.error.issues[0].message); // "Expected string, received number"

场景2:给基础类型加规则(常用修饰符)

目标

学会给规则加限制(如长度、范围),自定义错误提示

代码(可直接复制)
typescript 复制代码
import { z } from "zod";

// 字符串规则:长度限制 + 自定义提示 + 自动去空格
const usernameSchema = z.string()
  .min(3, "用户名至少3个字符") // 最小长度 + 自定义错误
  .max(20, "用户名最多20个字符") // 最大长度
  .trim(); // 自动去除首尾空格

// 数字规则:整数 + 范围限制
const ageSchema = z.number()
  .int("年龄必须是整数") // 仅允许整数
  .min(18, "年龄不能小于18") // 最小值
  .max(120, "年龄不能大于120"); // 最大值

// 可选字段 + 默认值(高频)
const rememberSchema = z.boolean()
  .optional() // 允许字段不存在/为 undefined
  .default(false); // 无值时自动填充默认值

// 测试默认值
console.log(rememberSchema.safeParse(undefined).data); // false

三、核心实战(最常用场景,覆盖90%需求)

场景3:对象校验(表单/接口核心)

目标

校验对象(如表单数据、接口返回),自动推导 TS 类型

代码(可直接复制)
typescript 复制代码
import { z } from "zod";

// 1. 定义对象 Schema(支持嵌套)
const UserSchema = z.object({
  name: z.string().min(2),
  age: z.number().int().min(18),
  email: z.string().email("邮箱格式错误").optional(), // 可选字段
  address: z.object({ // 嵌套对象
    city: z.string().nonempty("城市不能为空")
  })
});

// 2. 自动推导 TS 类型(Zod 核心价值,无需手写 interface)
type User = z.infer<typeof UserSchema>;

// 3. 通用校验函数(前端表单/接口通用)
const validateUser = (data: unknown) => {
  const result = UserSchema.safeParse(data);
  if (result.success) {
    return { valid: true, data: result.data as User };
  }
  // 格式化错误:适配前端表单({ 字段名: 错误提示 })
  const errors = result.error.issues.reduce((acc, issue) => {
    acc[issue.path.join(".")] = issue.message; // 嵌套字段如 "address.city"
    return acc;
  }, {} as Record<string, string>);
  return { valid: false, errors };
};

// 测试
const testData = {
  name: "张三",
  age: 20,
  address: { city: "" } // 城市为空,校验失败
};
console.log(validateUser(testData));
// 输出:{ valid: false, errors: { "address.city": "城市不能为空" } }

场景4:自定义规则(两次密码一致)

目标

解决内置规则满足不了的业务场景(如表单密码确认)

代码(可直接复制)
typescript 复制代码
import { z } from "zod";

// 定义规则 + 自定义校验
const PasswordSchema = z.object({
  password: z.string().min(6, "密码至少6位"),
  confirmPassword: z.string()
}).refine(
  (data) => data.password === data.confirmPassword, // 校验逻辑
  {
    message: "两次密码不一致",
    path: ["confirmPassword"] // 错误绑定到 confirmPassword 字段
  }
);

// 测试
const result = PasswordSchema.safeParse({
  password: "123456",
  confirmPassword: "654321"
});
console.log(result.error.issues[0].message); // "两次密码不一致"

场景5:数组/联合类型(补充高频)

目标

校验数组(如爱好列表)、多类型值(如 ID 支持字符串/数字)

代码(可直接复制)
typescript 复制代码
import { z } from "zod";

// 1. 数组校验:字符串数组,最少1个元素
const hobbiesSchema = z.array(z.string()).min(1, "至少选1个爱好");
console.log(hobbiesSchema.safeParse([]).error.issues[0].message); // "至少选1个爱好"

// 2. 联合类型:ID 支持 UUID 字符串 或 数字
const idSchema = z.union([z.string().uuid(), z.number().int()]);
console.log(idSchema.safeParse(123).success); // true
console.log(idSchema.safeParse("abc").success); // false

场景6:React + TS 表单完整实战

目标

整合所有用法,实现可直接运行的 React 表单校验

代码(可直接复制运行)
javascript 复制代码
import { useState } from "react";
import { z } from "zod";

// 1. 定义表单 Schema
const LoginSchema = z.object({
  username: z.string().min(3, "用户名至少3位"),
  password: z.string().min(6, "密码至少6位"),
  remember: z.boolean().optional().default(false)
});
// 自动推导表单类型
type LoginForm = z.infer<typeof LoginSchema>;

// 2. 封装校验函数
const validateLogin = (data: unknown) => {
  const result = LoginSchema.safeParse(data);
  if (result.success) return { valid: true, data: result.data };
  const errors = result.error.issues.reduce((acc, i) => ({ ...acc, [i.path[0]]: i.message }), {});
  return { valid: false, errors };
};

export default function LoginForm() {
  // 表单状态 + 错误状态
  const [form, setForm] = useState<LoginForm>({
    username: "",
    password: "",
    remember: false
  });
  const [errors, setErrors] = useState<Record<string, string>>({});

  // 输入变更处理
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value, type, checked } = e.target;
    setForm(prev => ({
      ...prev,
      [name]: type === "checkbox" ? checked : value
    }));
    // 输入时清除对应错误
    if (errors[name]) setErrors(prev => ({ ...prev, [name]: "" }));
  };

  // 提交校验
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    const { valid, errors: newErrors } = validateLogin(form);
    if (!valid) {
      setErrors(newErrors);
      return;
    }
    // 校验通过,调用接口
    console.log("提交数据:", form);
  };

  // 渲染表单
  return (
    <form onSubmit={handleSubmit} style={{ padding: 20 }}>
      <div style={{ marginBottom: 10 }}>
        <label>用户名:</label>
        <input
          name="username"
          value={form.username}
          onChange={handleChange}
        />
        {errors.username && <span style={{ color: "red" }}>{errors.username}</span>}
      </div>
      <div style={{ marginBottom: 10 }}>
        <label>密码:</label>
        <input
          type="password"
          name="password"
          value={form.password}
          onChange={handleChange}
        />
        {errors.password && <span style={{ color: "red" }}>{errors.password}</span>}
      </div>
      <div style={{ marginBottom: 10 }}>
        <input
          type="checkbox"
          name="remember"
          checked={form.remember}
          onChange={handleChange}
        />
        <label>记住我</label>
      </div>
      <button type="submit">登录</button>
    </form>
  );
}

四、新手避坑3条(必看)

  1. 优先用 safeParse 而非 parse:parse 校验失败会抛异常,需要 try/catch;safeParse 返回结果对象,前端更友好;
  2. 类型推导只写一行type 类型名 = z.infer<typeof Schema名>,无需手动定义 interface;
  3. 错误格式化固定写法issue.path.join(".") 处理嵌套字段,适配前端表单字段名。
相关推荐
宇擎智脑科技1 小时前
CopilotKit for LangGraph 深度解析:构建 Agent 原生应用的前端交互框架
前端·人工智能·交互
寒寒_2 小时前
使用Vue与Fabric.js创建图片标注工具
javascript·vue.js·fabric
mCell8 小时前
如何零成本搭建个人站点
前端·程序员·github
mCell9 小时前
为什么 Memo Code 先做 CLI:以及终端输入框到底有多难搞
前端·设计模式·agent
恋猫de小郭9 小时前
AI 在提高你工作效率的同时,也一直在增加你的疲惫和焦虑
前端·人工智能·ai编程
少云清10 小时前
【安全测试】2_客户端脚本安全测试 _XSS和CSRF
前端·xss·csrf
萧曵 丶10 小时前
Vue 中父子组件之间最常用的业务交互场景
javascript·vue.js·交互
银烛木10 小时前
黑马程序员前端h5+css3
前端·css·css3
m0_6070766010 小时前
CSS3 转换,快手前端面试经验,隔壁都馋哭了
前端·面试·css3