告别 TS 运行时类型漏洞!
Zod 是什么?
Zod 是一个
- 用简洁的语法定义「数据校验规则 + TypeScript 类型」(一份代码,双重收益);
- 校验前端表单、API 响应、环境变量等任意数据,返回清晰的错误信息;
- 零依赖、体积小,适配前端 /
Node.js 项目,是替代 Joi、Yup 的主流选择。
5 分钟快速上手(Vue3/Vite 项目为例)
步骤 1:安装
npm install zod
# 或 yarn/pnpm
pnpm add zod2
3
步骤 2:核心用法(定义 → 校验 → 提取类型)
Zod 的核心逻辑是:先定义 Schema 校验规则 → 用 Schema 校验数据 → 自动推导 TS 类型。
// src/utils/validate.ts
import { z } from 'zod'
// 1. 定义校验规则(Schema)
const UserSchema = z.object({
// 必选字符串,非空
username: z.string().min(2, '用户名至少2个字符').max(20),
// 可选数字,大于0
age: z.number().optional().positive('年龄必须为正数'),
// 邮箱格式校验
email: z.string().email('请输入正确的邮箱格式'),
// 枚举值限制
role: z.enum(['admin', 'user', 'guest'], '角色只能是admin/user/guest'),
// 嵌套对象
address: z.object({
city: z.string(),
street: z.string().optional()
})
})
// 2. 提取 TS 类型(无需手动写 interface)
type User = z.infer<typeof UserSchema>
// 3. 校验数据
function validateUser(data: unknown) {
try {
// 严格校验:不符合规则会抛错
const validData = UserSchema.parse(data)
console.log('校验通过', validData)
return { success: true, data: validData }
} catch (error) {
// 捕获错误并格式化
if (error instanceof z.ZodError) {
const errMsg = error.errors.map((item) => ({
field: item.path.join('.'), // 错误字段(如 address.city)
message: item.message // 错误提示
}))
return { success: false, errors: errMsg }
}
return { success: false, errors: [{ field: 'unknown', message: '未知错误' }] }
}
}
// 测试:校验合法数据
const validUser = {
username: '张三',
email: 'zhangsan@test.com',
role: 'user',
address: { city: '北京' }
}
console.log(validateUser(validUser)) // success: true
// 测试:校验非法数据
const invalidUser = {
username: '张', // 长度不足
email: '123', // 邮箱格式错误
role: 'super', // 枚举值错误
address: { city: 123 } // 类型错误
}
console.log(validateUser(invalidUser))
// success: false,errors 包含所有错误字段和提示2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
步骤 3:项目实战场景
场景 1:校验前端表单(Vue3 示例)
<!-- src/components/LoginForm.vue -->
<template>
<form @submit.prevent="submitForm">
<input
v-model="form.email"
placeholder="邮箱"
/>
<div v-if="errors.email">{{ errors.email }}</div>
<input
v-model="form.password"
placeholder="密码"
/>
<div v-if="errors.password">{{ errors.password }}</div>
<button type="submit">提交</button>
</form>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { z } from 'zod'
// 定义表单校验规则
const LoginSchema = z.object({
email: z.string().email('请输入正确的邮箱'),
password: z.string().min(6, '密码至少6位')
})
type LoginForm = z.infer<typeof LoginSchema>
// 表单数据
const form = ref<LoginForm>({ email: '', password: '' })
const errors = ref<Record<string, string>>({})
// 提交表单
const submitForm = () => {
// 清空之前的错误
errors.value = {}
// 校验数据(safeParse 不抛错,返回结果)
const result = LoginSchema.safeParse(form.value)
if (!result.success) {
// 格式化错误信息
result.error.errors.forEach((item) => {
errors.value[item.path[0]] = item.message
})
return
}
// 校验通过,调用接口
console.log('表单数据合法', result.data)
}
</script>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
场景 2:校验 API 响应
// src/api/user.ts
import { z } from 'zod'
import axios from 'axios'
// 定义 API 响应规则
const UserListSchema = z.array(
z.object({
id: z.number(),
name: z.string(),
avatar: z.string().url().optional() // 可选URL
})
)
// 请求接口并校验响应
async function getUserList() {
const res = await axios.get('/api/users')
// 校验响应数据,确保符合预期
const validData = UserListSchema.parse(res.data)
return validData
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
场景 3:校验环境变量(Vite 项目)
// src/utils/env.ts
import { z } from 'zod'
// 定义环境变量规则
const EnvSchema = z.object({
VITE_API_BASE: z.string().url('API地址必须是合法URL'),
VITE_GA_ID: z.string().optional()
})
// 校验 Vite 环境变量
const env = EnvSchema.parse(import.meta.env)
// 导出类型安全的环境变量
export default env2
3
4
5
6
7
8
9
10
11
12
13
高频实用 API 速查
| API 示例 | 作用 |
|---|---|
z.string().min(2) | 字符串,最小长度 2 |
z.number().int() | 整数 |
z.boolean() | 布尔值 |
z.array(z.string()) | 字符串数组 |
z.object({ a: z.string() }) | 对象校验 |
z.enum(['a', 'b']) | 枚举值限制 |
z.date() | 日期类型 |
z.any() | 任意类型 |
z.optional(z.string()) | 可选字符串 |
z.nullable(z.string()) | 可空字符串 |
schema.parse(data) | 严格校验,失败抛错 |
schema.safeParse(data) | 安全校验,返回结果(不抛错) |
z.infer<typeof schema> | 从 Schema 提取 TS 类型 |
总结
Zod 核心是「Schema 定义 → 数据校验 → 自动推导 TS 类型」,一份代码兼顾校验和类型;
常用场景:表单校验、API 响应校验、环境变量校验,适配前端 / Node.js;
核心 API:
z.object/z.string/z.number定义规则,parse/safeParse校验数据,z.infer提取类型。
上手关键:先定义
Schema,再用safeParse校验数据(避免抛错),最后格式化错误信息返回给用户。
前言:为什么 TypeScript 不够用?
日常开发中我们依赖 TypeScript 做静态类型约束,在代码编译阶段拦截类型写错、参数不匹配等问题,大幅降低低级 bug。但 TS 存在一个致命短板:类型校验仅存在编译阶段,项目打包运行后类型信息会完全丢失。
线上绝大多数动态数据都不受 TS 管控:后端接口返回 JSON、用户表单输入、第三方接口回调、本地存储读取的数据,全部属于运行时动态值,TS 无法提前校验。
举个直观的业务场景:我们定义用户信息类型
interface Customer {
username: string
birthday: number
}2
3
4
在本地硬编码赋值时,TS 会直接标红报错:
// 编辑器直接提示类型不匹配
const user: Customer = {
username: 2000,
birthday: '2000-01-01'
}2
3
4
5
但如果数据是接口返回的 JSON 字符串解析而来,情况完全不同:
// 模拟后端返回错误格式数据
const rawData = JSON.parse('{"username":1999,"birthday":"2000-05-20"}')
// 强制断言类型,TS不会抛出任何警告
const customer: Customer = rawData2
3
4
此时代码能正常打包运行,但实际数据和我们定义的类型完全相悖:用户名是数字、生日是字符串。后续代码读取customer.birthday做日期计算时,会直接抛出线上报错,这类问题很难提前复现排查。
简单总结痛点:TS 只能管编译期,运行时数据校验完全空白,而 Zod 就是为解决这个问题而生的工具。
Zod 核心介绍
Zod 是一款优先适配 TypeScript的 Schema 定义与数据校验库,核心目标是统一静态类型定义与运行时数据校验,实现「一次定义,两处复用」:
运行时校验:通过内置方法校验任意外部数据,自动捕获格式、类型、字段缺失问题;
自动 TS 类型推导:无需手动编写 interface/type,从 Schema 自动生成静态类型,杜绝类型重复定义。
快速上手:安装与基础校验
安装依赖
支持
npm install zod基础 Schema 定义
Zod 的核心开发思路:先定义数据模板 Schema,再用模板校验真实数据
import { z } from 'zod'
// 基础单字段规则
const NicknameSchema = z.string()
const ScoreSchema = z.number()
// 组合对象模板(用户信息)
const UserSchema = z.object({
nickname: NicknameSchema,
score: ScoreSchema
})2
3
4
5
6
7
8
9
10
11
两种数据校验方式
Zod 提供 parse 与 safeParse 两套校验方案,适配不同业务场景
方式 1:parse(校验失败直接抛出异常)
适合接口全局拦截、初始化校验,搭配 try/catch 捕获错误
// 合法数据,直接返回格式化后对象
UserSchema.parse({ nickname: '前端小周', score: 96 })
// 非法数据,抛出ZodError
UserSchema.parse({ nickname: 888, score: '满分' })2
3
4
5
抛出的错误包含完整字段定位信息:字段路径、预期类型、实际传入值,方便定位问题。
方式 2:safeParse(返回结果对象,不抛异常)
适合前端表单实时校验,无需捕获异常,通过success字段判断结果
// 校验成功
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: "类型错误,预期数字" }
]
}
}
*/2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
全量常用 Schema 类型详解
Zod 内置覆盖 JS/TS 全部基础数据结构,下面分大类讲解实战常用模板
基础原始类型
覆盖字符串、数字、布尔、日期、特殊空类型等
// 字符串
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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
对象类型(业务最常用)
用于固定字段结构化数据,配套多种扩展 API
const StudentSchema = z.object({
name: z.string(),
age: z.number()
})
// 扩展新增字段
const StudentWithEmail = StudentSchema.extend({ email: z.string().email() })
// 全部字段改为可选
StudentSchema.partial()
// 全部字段强制必填
StudentSchema.required()2
3
4
5
6
7
8
9
10
11
数组 & 元组
数组:统一约束数组内所有元素类型,长度不固定
const numList = z.array(z.number())
numList.parse([12, 34, 56]) // 通过
numList.parse(['12', '34']) // 报错2
3
元组:固定长度、固定顺序、每个位置单独约束类型
const tagTuple = z.tuple([z.string(), z.number()])
tagTuple.parse(['前端', 1]) // 通过
tagTuple.parse([1, '前端']) // 顺序错误,报错2
3
集合类型 Record / Map / Set
Record:动态键值对象,所有键 / 值统一类型(和 object 区分:object 是固定字段,record 是任意字段)
// 键为数字,值为字符串
const Dict = z.record(z.number(), z.string())
Dict.parse({ 1: '首页', 2: '详情页' }) // 通过
Dict.parse({ 1: 999 }) // 值类型错误,报错2
3
4
Map/Set:校验 ES 原生集合对象
const strMap = z.map(z.string(), z.number())
const numSet = z.set(z.number())
numSet.parse(new Set([1, 2, 3])) // 通过2
3
字面量、枚举校验
字面量:仅允许固定单一值
const statusDraft = z.literal('draft')
statusDraft.parse('draft') // 通过
statusDraft.parse('publish') // 报错2
3
字符串枚举:限定可选字符串列表
const ArticleStatus = z.enum(['draft', 'publish', 'archived'])
ArticleStatus.parse('draft') // 通过2
原生 TS 枚举:校验自定义 enum
enum OrderState {
Pending,
Completed
}
const OrderEnum = z.nativeEnum(OrderState)
OrderEnum.parse(OrderState.Pending)2
3
4
5
6
Schema 修饰与组合语法
真实业务中字段不会全是必填固定类型,Zod 提供修饰符、组合语法处理可选、空值、多类型合并场景
空值修饰符
.optional():允许当前值为定义类型 /undefined(表单非必填字段)
const optStr = z.string().optional()
optStr.parse('测试')
optStr.parse(undefined)2
3
.nullable():允许当前值为定义类型 /null
const nullStr = z.string().nullable()
nullStr.parse(null)2
.nullish():同时支持 null、undefined、原类型
const allEmptyStr = z.string().nullish()
allEmptyStr.parse(null)
allEmptyStr.parse(undefined)2
3
.default():传入 undefined 时自动填充默认值
const strWithDefault = z.string().default('无备注')
strWithDefault.parse(undefined) // 输出 "无备注"2
多 Schema 组合
联合类型 union:满足任意一种 Schema 即可
const strOrNum = z.union([z.string(), z.number()])
strOrNum.parse(99)
strOrNum.parse('99')2
3
交叉类型 intersection:必须同时满足两个 Schema(合并对象字段)
const HasName = z.object({ name: z.string() })
const HasScore = z.object({ score: z.number() })
const Student = z.intersection(HasName, HasScore)2
3
判别联合 discriminatedUnion:带标识字段的多对象联合,报错精准、校验性能更高适用于多类型区分对象(如不同类型订单、不同组件配置)
// 两种商品类型,通过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 })2
3
4
5
6
7
8
9
10
11
精细化自定义校验规则
基础类型仅能校验基础格式,业务场景需要长度、正则、跨字段对比等自定义约束
内置链式校验方法
字符串、数字自带大量业务常用校验规则,支持自定义报错文案
// 用户名: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
3
4
5
6
7
8
9
10
11
12
13
14
单字段自定义校验 refine
内置规则无法满足时,自定义校验逻辑,自定义错误提示
// 密码最少8位
const pwdSchema = z.string().refine((val) => val.length >= 8, {
message: '密码长度不能小于8位'
})2
3
4
跨字段全局校验 superRefine
读取整个对象数据,实现多字段联动校验(如密码二次确认)
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: '两次输入密码不一致'
})
}
})2
3
4
5
6
7
8
9
10
11
12
13
14
15
错误格式化 format ()
原始 ZodError 的错误结构层级深,不适合前端表单渲染,format()会自动转为「字段为 key」的对象结构
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字段错误文案
}2
3
4
5
6
7
8
9
10
11
数据转换:coerce /transform/pipe
很多场景输入数据格式不符合需求,Zod 支持在校验前后自动转换数据,无需手动处理
coerce 强制类型转换
常用于表单、URL 参数(全部为字符串,需要转数字 / 日期)
const coerceNum = z.coerce.number()
coerceNum.parse('96') // 自动转为数字962
支持:z.coerce.string()、z.coerce.boolean()、z.coerce.date()
transform 校验后数据加工
校验通过后,对值做格式化、裁剪、计算等操作
// 自动去除首尾空格
const trimText = z.string().transform((val) => val.trim())
trimText.parse(' 掘金文章 ') // 输出 "掘金文章"2
3
pipe 多 Schema 管道串联
将多个 Schema 按顺序串联,前一个校验结果传入下一个 Schema
// 数字转字符串
const numToStr = z.number().pipe(z.string())
numToStr.parse(2026) // 输出 "2026"2
3
TypeScript 类型自动推断(Zod 核心亮点)
不用手动写 interface,通过z.infer直接从 Schema 提取 TS 静态类型,保证运行校验和静态类型完全同步
const ArticleSchema = z.object({
title: z.string(),
readCount: z.number()
})
// 自动推导静态类型
type Article = z.infer<typeof ArticleSchema>
/*
等价于手动定义:
interface Article {
title: string;
readCount: number;
}
*/2
3
4
5
6
7
8
9
10
11
12
13
14
区分输入 / 输出类型 z.input/z.output
如果使用 transform、pipe 转换数据,输入原始值和最终输出值类型不一致,可分别提取:
const LenSchema = z.string().transform((val) => val.length)
type RawInput = z.input<typeof LenSchema> // string
type FinalOutput = z.output<typeof LenSchema> // number2
3
高阶实用特性
递归树形结构 z.lazy
处理无限嵌套树形数据(分类、评论、菜单),解决 Schema 自引用报错
// 树形分类结构
const CategorySchema: z.ZodType<any> = z.object({
label: z.string(),
children: z.array(z.lazy(() => CategorySchema))
})2
3
4
5
对象多余字段控制
默认会保留未定义的额外字段,三种模式按需切换
// strict:禁止传入额外字段,多字段直接报错
z.object({ name: z.string() }).strict()
// strip:自动剔除多余字段,只保留定义字段
z.object({ name: z.string() }).strip()
// passthrough:保留额外字段,不做校验
z.object({ name: z.string() }).passthrough()2
3
4
5
6
7
8
Schema 描述 describe
给字段添加注释,用于生成接口文档、JSON Schema 导出
const User = z.object({
username: z.string().describe('用户账号'),
createTime: z.date().describe('账号创建时间')
})2
3
4
生态集成实战
导出 JSON Schema
Zod4 内置toJSONSchema,将 Schema 转为标准 JSON Schema,用于 OpenAPI 接口文档、配置校验
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
3
4
搭配 React Hook Form 表单校验
前端表单最常用组合,zodResolver直接对接表单库,自动绑定错误信息
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>
)
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
总结
TypeScript 解决了编译期类型安全,但无法管控接口、表单、本地存储等运行时动态数据,而 Zod 完美填补这块空白。只需要定义一套 Schema,就能同时拿到静态 TS 类型和运行时数据校验能力,大幅减少线上类型异常、字段缺失、格式错误等 bug,同时简化表单、接口参数校验代码。
不管是前端表单、前后端接口入参出参校验、配置文件解析,Zod 都是现代 TS 项目的标准解决方案。如果你的项目还在手动写 if 判断校验数据,不妨尝试接入 Zod,一次性简化所有数据校验逻辑。
