前言:为什么 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 定义与数据校验库,核心目标是统一静态类型定义与运行时数据校验,实现「一次定义,两处复用」:
- 运行时校验:通过内置方法校验任意外部数据,自动捕获格式、类型、字段缺失问题;
- 自动 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 提供parse与safeParse两套校验方案,适配不同业务场景
方式 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
- Record:动态键值对象,所有键 / 值统一类型(和 object 区分:object 是固定字段,record 是任意字段)
typescript
// 键为数字,值为字符串
const Dict = z.record(z.number(), z.string());
Dict.parse({ 1: "首页", 2: "详情页" }); // 通过
Dict.parse({ 1: 999 }); // 值类型错误,报错
- 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 组合
- 联合类型 union:满足任意一种 Schema 即可
typescript
const strOrNum = z.union([z.string(), z.number()]);
strOrNum.parse(99);
strOrNum.parse("99");
- 交叉类型 intersection:必须同时满足两个 Schema(合并对象字段)
typescript
const HasName = z.object({ name: z.string() });
const HasScore = z.object({ score: z.number() });
const Student = z.intersection(HasName, HasScore);
- 判别联合 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,一次性简化所有数据校验逻辑。