Zod 基础
到目前为止出现的三个 bug,形状都一样:一个值传了进来,代码用了它,但没有人先检查一下。
在每个出问题的地方各自修一次也能凑效,但这样永远修不完。每多一个新的使用位置,就多一个要记住检查的地方。
另一种做法是:在值到达的那一刻,就用一份"你期望它是什么样"的描述去核对它。这份描述就是 schema(模式),本节要用来写它的库就是 Zod。
什么是 schema
想象机场的安检扫描仪。你的包从一头放进去,机器已经设置好了规则:不能带利器,液体不能超过一定体积。符合规则的包会从另一头原样出来,不符合的就过不去。
schema 就是机器的这些设置,而 parse(解析)就是让包过一遍扫描仪的过程。
先安装它,再引入:
npm install zodimport * as z from 'zod'最简单的 schema 就是针对单个值的一条规则:
const teacherSchema = z.string()
const teacher = 'Jonathan'
console.log(teacherSchema.parse(teacher))
// 'Jonathan'parse 把数据交给机器。数据符合规则,就会原样返回。
给它一个不符合规则的值,机器就会拦下:
teacherSchema.parse(12345)
// ZodError: Invalid input: expected string, received number这就是整个模型:描述什么是可接受的,把值传进去,要么拿回这个值,要么拿到一个错误。
schema 是你会接受什么样数据的描述。parse(解析)是拿某样东西去和这份描述核对的动作。schema 只写一次,之后可以随便用它来 parse 多少次都行。
描述一个对象
一位老师很少只是一个字符串就能描述完的。给 schema 一个形状:
const teacherSchema = z.object({
name: z.string(),
age: z.number(),
})
const teacher = {
name: 'Jonathan',
age: 21,
}
console.log(teacherSchema.parse(teacher))
// { name: 'Jonathan', age: 21 }z.object() 里的每一项都是一个键,它的值就是这个键对应的 schema。现在机器期望的是一个对象,其中 name 是任意字符串,age 是任意数字。
破坏其中任何一条,报错都会精确指出是哪一条:
teacherSchema.parse({ name: 'Jonathan', age: '21' })
// ZodError: Invalid input: expected number, received stringZod 提供了你期望的那些基本类型,z.string()、z.number()、z.boolean(),而对象可以像你的数据一样,一层套一层地嵌套。
所以同一个思路既能描述一个两字段的表单,也能描述一个结构层层叠叠的 API 响应。你始终只是在一次描述一层。
为什么这项检查必须在运行时进行
TypeScript 也能描述形状:
type Teacher = {
name: string
age: number
}这看起来很像 schema,做的事却完全不同。TypeScript 是在你写代码的时候检查类型。编译成 JavaScript 之后,所有类型注解都会被抹掉,没有任何东西能留到运行中的程序里。
这对于你自己代码产生的值来说没问题,但对于不是你代码产生的值就没用了。请求体、API 响应、表单提交:这些都是在程序运行时才到达的,而那时类型早就不存在了。TypeScript 假设你的数据是对的,Zod 才会去检查。
schema 描述的是一种类型,而且这种描述在数据真正出现的那一刻依然有效。
TypeScript 是你写代码时和你之间的对话。schema 是程序运行时和数据之间的对话。
动手试一试
从头写一遍,不要照抄 teacher 那个例子。
- 引入 Zod。
- 创建一个
characterSchema,包含两个键:name(字符串)和episode(数字)。 - 定义一个
character对象,name 为'Luke Skywalker',episode 为4。 - 用这个 schema 校验 character,并把结果打印出来。
- 把
episode改成字符串'4',在运行之前先猜猜会发生什么。
对照一下你的答案
import * as z from 'zod'
const characterSchema = z.object({
name: z.string(),
episode: z.number(),
})
const character = {
name: 'Luke Skywalker',
episode: 4,
}
console.log(characterSchema.parse(character))
// { name: 'Luke Skywalker', episode: 4 }两个键意味着要用 z.object(),而不是单一的基本类型,每个键都有自己对应的 schema。
当 episode 是 '4' 时,parse 会抛出 ZodError: Invalid input: expected number, received string。字符串 '4' 不是数字,Zod 不会悄悄帮你转换。如果要有意地转换,那是另外一条指令,下一章会讲到。
顺带一提,第 4 集是对的。卢克·天行者第一次出现是在 1977 年最初那部《星球大战》电影里,后来这部电影被编号为第四集。
接下来的方向
现在的 schema 只做一件事:接受一个值,或者抛出异常。这足以描述数据,但还不足以用来搭建系统。
类型推断与输入转换 会补上让它变得实用的两块拼图。同一个 schema 也能把类型交给 TypeScript,形状只需要写一次。而解析失败时,你能拿到一个可供检查的结果,而不是必须捕获的异常。

