推导类型与转换输入数据
一个"要么接受要么报错"的 schema 已经足够描述数据了。但要真正用它来搭建东西,还需要两样东西:一种在自己代码里使用这个数据形状的方式,以及一种不靠抛异常就能处理失败的方式。
从一个描述教师的 schema 开始:
import * as z from 'zod'
const teacherSchema = z.object({
name: z.string(),
age: z.number(),
})一个 schema,两份用处
在 TypeScript 里,你通常得把这个形状再写一遍:
type Teacher = {
name: string
age: number
}这样一来,同一个形状就存在于两个地方,只要有人给其中一个加了字段,两边就会慢慢对不上。
z.infer 直接从 schema 上取出类型:
type Teacher = z.infer<typeof teacherSchema>
// { name: string; age: number }给 schema 加一个字段,类型会自动跟着变。schema 才是那份定义,TypeScript 只是读取它。
typeof 看起来有点奇怪,因为它不是你熟悉的 JavaScript 的 typeof。这是 TypeScript 自己的版本,它在类型层面问的是"这个值的类型是什么"。 你可以把整行话当成一句话来理解:把这个 schema 描述的类型给我。
不靠异常处理失败
parse 会抛异常。这适合那种"请求一旦有问题就该整个中断"的边界场景,但要是你想看看到底哪里出了错,它就完全不合适。
safeParse 返回的是一个结果对象:
const result = teacherSchema.safeParse({ name: 'Jonathan', age: 21 })
console.log(result)
// { success: true, data: { name: 'Jonathan', age: 21 } }失败时,返回的形状会变:
const result = teacherSchema.safeParse({ name: 'Jonathan', age: '21' })
console.log(result.success)
// false成功时给你 success: true 和 data。失败时给你 success: false 和 error。这样表单就能先判断结果,再决定怎么做:
if (result.success) {
console.log('可以安全地继续发送:', result.data)
} else {
console.log('把哪里出错展示给用户看:', result.error.issues)
}parse 会抛异常,除非你捕获它,否则整个流程都会中断。safeParse 返回一个带 success 标志的对象,你可以先判断,再继续往下走。
收窄你能接受的范围
类型只是一个粗粒度的过滤器。z.number() 对 -4 和 9e99 一视同仁,可这两个都不像是教师的年龄。
一串方法可以链在 schema 上,把它收得更紧:
const teacherSchema = z.object({
name: z.string(),
age: z.number().min(18),
isAmerican: z.boolean().optional(),
id: z.number().default(() => Math.random()),
}).min(18)拒绝任何低于 18 的值。.gte()和.lte()用明确的比较做同样的事。.optional()允许这个字段完全缺失。.default()在字段缺失时提供一个值,这样解析出来的对象总会有这个字段。
Zod 还内置了常见格式的检查,验证邮箱一个调用就够了:
const contactSchema = z.object({
email: z.email(),
})
contactSchema.parse({ email: '[email protected]' }) // 没问题
contactSchema.parse({ email: 'jabbahuttcorp.com' }) // ZodError这就是这个库所说的"声明式":你描述想要的结果,检查的活儿它替你干。
转换传入的数据
HTML 表单字段发送的都是字符串。每一个都是,包括那个标着"年龄"、旁边还带着数字调节按钮的字段。
所以一个期待 z.number() 的 schema 会拒绝完全合理的表单输入,因为 '13' 是个字符串。类型转换会先转换,再验证:
const characterSchema = z.object({
name: z.string(),
episode: z.coerce.number(),
})
characterSchema.parse({ name: 'Luke Skywalker', episode: '4' })
// { name: 'Luke Skywalker', episode: 4 }进去的是字符串,出来的是数字,这个字段之后的任何检查都是针对这个数字进行的。
所以 z.coerce.number().min(18) 是先把文本转成数字,再判断这个数字是否至少是 18。
动手试试
从下面这个 schema 和对象出发:
const characterSchema = z.object({
name: z.string(),
episode: z.number(),
})
const character = {
name: 'Jabba the Hutt',
episode: '6',
}做四处修改:
- 创建一个从 schema 推导出的
Character类型,并用它给这个对象做类型标注。 - 给 schema 加一个可选的布尔字段
isJedi。 - 让
episode能接受字符串'6',并把它存成数字。 - 把验证是否通过的结果,作为一个单独的布尔值打印出来。
对照一下你的答案
type Character = z.infer<typeof characterSchema>
const characterSchema = z.object({
name: z.string(),
episode: z.coerce.number(),
isJedi: z.boolean().optional(),
})
const character: Character = {
name: 'Jabba the Hutt',
episode: '6',
}
console.log(characterSchema.safeParse(character).success)
// true针对这四处修改,有四点说明:
z.infer<typeof characterSchema>直接从 schema 上读取类型,所以加上isJedi之后类型会自动更新,不用再改第二个地方。.optional()表示这个字段可以缺失,这也是为什么character不需要isJedi字段照样能通过验证。z.coerce.number()先转换再检查,所以字符串'6'会变成数字6。.success就是那个布尔值。单独打印safeParse(...)会输出整个结果对象,而parse给你的要么是数据要么是异常,都不是布尔值。
有一点值得留意:由于 episode 做了类型转换,标注的类型说是 number,而对象字面量里存的却是 '6'。这正是输入类型和输出类型不一致的地方,也是为什么该读 result.data 而不是原始对象的原因。
接下来去哪儿
safeParse 失败时会返回一个 error,但到目前为止,我们只是确认了它的存在,还没真正去读它。
读懂验证错误 会把它打开来看:哪个字段出了问题,问题出在哪,以及怎么把它变成用户能看懂、能照着行动的提示信息。

