告别 TS 运行时类型漏洞!Zod 完整入门实战教程(前端 / 全栈必备)

前言:为什么 TypeScript 不够用?

日常开发中我们依赖 TypeScript 做静态类型约束,在代码编译阶段拦截类型写错、参数不匹配等问题,大幅降低低级 bug。但 TS 存在一个致命短板:类型校验仅存在编译阶段,项目打包运行后类型信息会完全丢失

线上绝大多数动态数据都不受 TS 管控:后端接口返回 JSON、用户表单输入、第三方接口回调、本地存储读取的数据,全部属于运行时动态值,TS 无法提前校验。

举个直观的业务场景:我们定义用户信息类型

typescript 复制代码
interface Customer {
  username: string;
  birthday: number;
}

在本地硬编码赋值时,TS 会直接标红报错:

typescript 复制代码
// 编辑器直接提示类型不匹配
const user: Customer = {
  username: 2000,
  birthday: "2000-01-01"
}

但如果数据是接口返回的 JSON 字符串解析而来,情况完全不同:

typescript 复制代码
// 模拟后端返回错误格式数据
const rawData = JSON.parse('{"username":1999,"birthday":"2000-05-20"}');
// 强制断言类型,TS不会抛出任何警告
const customer: Customer = rawData;

此时代码能正常打包运行,但实际数据和我们定义的类型完全相悖:用户名是数字、生日是字符串。后续代码读取customer.birthday做日期计算时,会直接抛出线上报错,这类问题很难提前复现排查。

简单总结痛点:TS 只能管编译期,运行时数据校验完全空白,而 Zod 就是为解决这个问题而生的工具。

一、Zod 核心介绍

Zod 是一款优先适配 TypeScript的 Schema 定义与数据校验库,核心目标是统一静态类型定义与运行时数据校验,实现「一次定义,两处复用」:

  1. 运行时校验:通过内置方法校验任意外部数据,自动捕获格式、类型、字段缺失问题;
  2. 自动 TS 类型推导:无需手动编写 interface/type,从 Schema 自动生成静态类型,杜绝类型重复定义。

二、快速上手:安装与基础校验

1. 安装依赖

支持 npm、pnpm、yarn 任意包管理器

bash 复制代码
npm install zod

2. 基础 Schema 定义

Zod 的核心开发思路:先定义数据模板 Schema,再用模板校验真实数据

typescript 复制代码
import { z } from "zod";

// 基础单字段规则
const NicknameSchema = z.string();
const ScoreSchema = z.number();

// 组合对象模板(用户信息)
const UserSchema = z.object({
  nickname: NicknameSchema,
  score: ScoreSchema
});

3. 两种数据校验方式

Zod 提供parsesafeParse两套校验方案,适配不同业务场景

方式 1:parse(校验失败直接抛出异常)

适合接口全局拦截、初始化校验,搭配 try/catch 捕获错误

typescript 复制代码
// 合法数据,直接返回格式化后对象
UserSchema.parse({ nickname: "前端小周", score: 96 });

// 非法数据,抛出ZodError
UserSchema.parse({ nickname: 888, score: "满分" });

抛出的错误包含完整字段定位信息:字段路径、预期类型、实际传入值,方便定位问题。

方式 2:safeParse(返回结果对象,不抛异常)

适合前端表单实时校验,无需捕获异常,通过success字段判断结果

typescript 复制代码
// 校验成功
const successRes = UserSchema.safeParse({ nickname: "小李", score: 82 });
// { success: true, data: { nickname: "小李", score: 82 } }

// 校验失败
const failRes = UserSchema.safeParse({ nickname: 666, score: "九十" });
/*
{
  success: false,
  error: ZodError {
    issues: [
      { path: ["nickname"], expected: "string", message: "类型错误,预期字符串" },
      { path: ["score"], expected: "number", message: "类型错误,预期数字" }
    ]
  }
}
*/

三、全量常用 Schema 类型详解

Zod 内置覆盖 JS/TS 全部基础数据结构,下面分大类讲解实战常用模板

1. 基础原始类型

覆盖字符串、数字、布尔、日期、特殊空类型等

typescript 复制代码
// 字符串
const text = z.string();
text.parse("前端开发"); // 通过
text.parse(123); // 报错

// 数字
const num = z.number();
num.parse(60); // 通过
num.parse("60"); // 报错

// 布尔、BigInt、Date
const flag = z.boolean();
const bigNum = z.bigint();
const day = z.date();
day.parse(new Date()); // 通过
day.parse("2026-06-30"); // 报错

// 空值相关
z.null();
z.undefined();
z.any(); // 不做任何校验
z.unknown(); // 仅做类型占位,需二次校验
z.never(); // 不允许传入任何值

2. 对象类型(业务最常用)

用于固定字段结构化数据,配套多种扩展 API

typescript 复制代码
const StudentSchema = z.object({
  name: z.string(),
  age: z.number()
});

// 扩展新增字段
const StudentWithEmail = StudentSchema.extend({ email: z.string().email() });
// 全部字段改为可选
StudentSchema.partial();
// 全部字段强制必填
StudentSchema.required();

3. 数组 & 元组

  • 数组:统一约束数组内所有元素类型,长度不固定
typescript 复制代码
const numList = z.array(z.number());
numList.parse([12, 34, 56]); // 通过
numList.parse(["12", "34"]); // 报错
  • 元组:固定长度、固定顺序、每个位置单独约束类型
typescript 复制代码
const tagTuple = z.tuple([z.string(), z.number()]);
tagTuple.parse(["前端", 1]); // 通过
tagTuple.parse([1, "前端"]); // 顺序错误,报错

4. 集合类型 Record / Map / Set

  1. Record:动态键值对象,所有键 / 值统一类型(和 object 区分:object 是固定字段,record 是任意字段)
typescript 复制代码
// 键为数字,值为字符串
const Dict = z.record(z.number(), z.string());
Dict.parse({ 1: "首页", 2: "详情页" }); // 通过
Dict.parse({ 1: 999 }); // 值类型错误,报错
  1. Map/Set:校验 ES 原生集合对象
typescript 复制代码
const strMap = z.map(z.string(), z.number());
const numSet = z.set(z.number());
numSet.parse(new Set([1, 2, 3])); // 通过

5. 字面量、枚举校验

字面量:仅允许固定单一值

typescript 复制代码
const statusDraft = z.literal("draft");
statusDraft.parse("draft"); // 通过
statusDraft.parse("publish"); // 报错

字符串枚举:限定可选字符串列表

typescript 复制代码
const ArticleStatus = z.enum(["draft", "publish", "archived"]);
ArticleStatus.parse("draft"); // 通过

原生 TS 枚举:校验自定义 enum

typescript 复制代码
enum OrderState {
  Pending,
  Completed
}
const OrderEnum = z.nativeEnum(OrderState);
OrderEnum.parse(OrderState.Pending);

四、Schema 修饰与组合语法

真实业务中字段不会全是必填固定类型,Zod 提供修饰符、组合语法处理可选、空值、多类型合并场景

1. 空值修饰符

  • .optional():允许当前值为定义类型 /undefined(表单非必填字段)
typescript 复制代码
const optStr = z.string().optional();
optStr.parse("测试");
optStr.parse(undefined);
  • .nullable():允许当前值为定义类型 /null
typescript 复制代码
const nullStr = z.string().nullable();
nullStr.parse(null);
  • .nullish():同时支持 null、undefined、原类型
typescript 复制代码
const allEmptyStr = z.string().nullish();
allEmptyStr.parse(null);
allEmptyStr.parse(undefined);
  • .default():传入 undefined 时自动填充默认值
typescript 复制代码
const strWithDefault = z.string().default("无备注");
strWithDefault.parse(undefined); // 输出 "无备注"

2. 多 Schema 组合

  1. 联合类型 union:满足任意一种 Schema 即可
typescript 复制代码
const strOrNum = z.union([z.string(), z.number()]);
strOrNum.parse(99);
strOrNum.parse("99");
  1. 交叉类型 intersection:必须同时满足两个 Schema(合并对象字段)
typescript 复制代码
const HasName = z.object({ name: z.string() });
const HasScore = z.object({ score: z.number() });
const Student = z.intersection(HasName, HasScore);
  1. 判别联合 discriminatedUnion:带标识字段的多对象联合,报错精准、校验性能更高适用于多类型区分对象(如不同类型订单、不同组件配置)
typescript 复制代码
// 两种商品类型,通过type字段区分
const Book = z.object({
  type: z.literal("book"),
  page: z.number()
});
const Food = z.object({
  type: z.literal("food"),
  weight: z.number()
});
const Goods = z.discriminatedUnion("type", [Book, Food]);
Goods.parse({ type: "book", page: 200 });

五、精细化自定义校验规则

基础类型仅能校验基础格式,业务场景需要长度、正则、跨字段对比等自定义约束

1. 内置链式校验方法

字符串、数字自带大量业务常用校验规则,支持自定义报错文案

typescript 复制代码
// 用户名:3-12位小写字母
const username = z.string()
  .min(3, { message: "用户名最少3位字符" })
  .max(12, { message: "用户名最多12位字符" })
  .regex(/^[a-z]+$/, { message: "仅允许小写英文" });

// 年龄:0~120之间整数
const userAge = z.number()
  .int({ message: "年龄必须为整数" })
  .min(0)
  .max(120);

// 邮箱、链接、UUID快捷校验
z.string().email();
z.string().url();
z.string().uuid();

2. 单字段自定义校验 refine

内置规则无法满足时,自定义校验逻辑,自定义错误提示

typescript 复制代码
// 密码最少8位
const pwdSchema = z.string().refine(val => val.length >= 8, {
  message: "密码长度不能小于8位"
});

3. 跨字段全局校验 superRefine

读取整个对象数据,实现多字段联动校验(如密码二次确认)

typescript 复制代码
const LoginForm = z.object({
  password: z.string(),
  confirmPwd: z.string()
}).superRefine((formData, ctx) => {
  if (formData.password !== formData.confirmPwd) {
    // 指定错误绑定到confirmPwd字段
    ctx.addIssue({
      code: "custom",
      path: ["confirmPwd"],
      message: "两次输入密码不一致"
    });
  }
});

4. 错误格式化 format ()

原始 ZodError 的错误结构层级深,不适合前端表单渲染,format()会自动转为「字段为 key」的对象结构

typescript 复制代码
const FormSchema = z.object({
  name: z.string().min(3),
  age: z.number().min(18)
});
const res = FormSchema.safeParse({ name: "A", age: 16 });

if (!res.success) {
  // 格式化错误,直接绑定表单UI
  const errInfo = res.error.format();
  console.log(errInfo.name._errors[0]); // 获取name字段错误文案
}

六、数据转换:coerce /transform/pipe

很多场景输入数据格式不符合需求,Zod 支持在校验前后自动转换数据,无需手动处理

1. coerce 强制类型转换

常用于表单、URL 参数(全部为字符串,需要转数字 / 日期)

typescript 复制代码
const coerceNum = z.coerce.number();
coerceNum.parse("96"); // 自动转为数字96

支持:z.coerce.string()z.coerce.boolean()z.coerce.date()

2. transform 校验后数据加工

校验通过后,对值做格式化、裁剪、计算等操作

typescript 复制代码
// 自动去除首尾空格
const trimText = z.string().transform(val => val.trim());
trimText.parse("  掘金文章  "); // 输出 "掘金文章"

3. pipe 多 Schema 管道串联

将多个 Schema 按顺序串联,前一个校验结果传入下一个 Schema

typescript 复制代码
// 数字转字符串
const numToStr = z.number().pipe(z.string());
numToStr.parse(2026); // 输出 "2026"

七、TypeScript 类型自动推断(Zod 核心亮点)

不用手动写 interface,通过z.infer直接从 Schema 提取 TS 静态类型,保证运行校验和静态类型完全同步

typescript 复制代码
const ArticleSchema = z.object({
  title: z.string(),
  readCount: z.number()
});

// 自动推导静态类型
type Article = z.infer<typeof ArticleSchema>;
/*
等价于手动定义:
interface Article {
  title: string;
  readCount: number;
}
*/

区分输入 / 输出类型 z.input/z.output

如果使用 transform、pipe 转换数据,输入原始值和最终输出值类型不一致,可分别提取:

typescript 复制代码
const LenSchema = z.string().transform(val => val.length);
type RawInput = z.input<typeof LenSchema>; // string
type FinalOutput = z.output<typeof LenSchema>; // number

八、高阶实用特性

1. 递归树形结构 z.lazy

处理无限嵌套树形数据(分类、评论、菜单),解决 Schema 自引用报错

typescript 复制代码
// 树形分类结构
const CategorySchema: z.ZodType<any> = z.object({
  label: z.string(),
  children: z.array(z.lazy(() => CategorySchema))
});

2. 对象多余字段控制

默认会保留未定义的额外字段,三种模式按需切换

typescript 复制代码
// strict:禁止传入额外字段,多字段直接报错
z.object({ name: z.string() }).strict();

// strip:自动剔除多余字段,只保留定义字段
z.object({ name: z.string() }).strip();

// passthrough:保留额外字段,不做校验
z.object({ name: z.string() }).passthrough();

3. Schema 描述 describe

给字段添加注释,用于生成接口文档、JSON Schema 导出

typescript 复制代码
const User = z.object({
  username: z.string().describe("用户账号"),
  createTime: z.date().describe("账号创建时间")
});

九、生态集成实战

1. 导出 JSON Schema

Zod4 内置toJSONSchema,将 Schema 转为标准 JSON Schema,用于 OpenAPI 接口文档、配置校验

typescript 复制代码
import { z } from "zod";
const User = z.object({ name: z.string(), age: z.number().min(18) });
const jsonSchema = z.toJSONSchema(User);
console.log(jsonSchema);

2. 搭配 React Hook Form 表单校验

前端表单最常用组合,zodResolver直接对接表单库,自动绑定错误信息

tsx 复制代码
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

const LoginSchema = z.object({
  email: z.string().email("邮箱格式错误"),
  password: z.string().min(8, "密码至少8位")
});

export default function LoginPage() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: zodResolver(LoginSchema)
  });

  const submit = (data) => console.log("表单数据", data);
  return (
    <form onSubmit={handleSubmit(submit)}>
      <input {...register("email")} placeholder="请输入邮箱" />
      {errors.email && <span className="err">{errors.email.message}</span>}

      <input type="password" {...register("password")} placeholder="请输入密码"/>
      {errors.password && <span className="err">{errors.password.message}</span>}
      <button type="submit">登录</button>
    </form>
  )
}

总结

TypeScript 解决了编译期类型安全,但无法管控接口、表单、本地存储等运行时动态数据,而 Zod 完美填补这块空白。

只需要定义一套 Schema,就能同时拿到静态 TS 类型运行时数据校验能力,大幅减少线上类型异常、字段缺失、格式错误等 bug,同时简化表单、接口参数校验代码。

不管是前端表单、前后端接口入参出参校验、配置文件解析,Zod 都是现代 TS 项目的标准解决方案。如果你的项目还在手动写 if 判断校验数据,不妨尝试接入 Zod,一次性简化所有数据校验逻辑。

相关推荐
the_answer2 小时前
Webpack vs Vite 深度对比分析
前端·webpack
转转技术团队2 小时前
验证码识别实战:前端不写页面,改训模型了?
前端
MomentYY2 小时前
Temperature:AI 的“脑洞旋钮”
前端·llm·ai编程
远航_2 小时前
OpenSpec 完整详细介绍
前端·后端
召钱熏2 小时前
状态枚举正确≠渲染正确:一个语音按钮的状态机边界修复实录
android·前端
SkyWalking中文站2 小时前
认识 Horizon UI · 1/17:SkyWalking 新一代可观测性控制台
运维·前端·监控
cidy_982 小时前
Dify 操作教程:工作流编排 & Chat 对话编排
前端·工作流引擎
tangdou3690986552 小时前
AI真好玩系列-2分钟快速了解DeepAgents | Quick Guide to DeepAgents in 2 Minutes
前端·javascript·后端
张元清2 小时前
React useIntersectionObserver Hook:懒加载与可见性检测(2026)
javascript·react.js