科技前沿 推荐Skill推荐TypeScript开发工具

Zod:TypeScript 项目的最后一道防线

📅 2026-07-13

你写的 API 路由接收了一个 POST 请求,req.body 是用户传来的数据。它包含 title、category、tags,但它是否真的符合你的数据库约束?

90% 的 Node.js 项目靠手动 if 检查解决这个问题。剩下的 10% 用 Zod。

Zod(43,247 个 GitHub stars,MIT 协议)是一个 TypeScript 优先的 Schema 验证库。你定义一个 schema,用一行 .parse() 验证数据——验证通过后返回一个带完整类型推断的结果,而不是 any。

三个核心亮点

1. 一个 Schema 同时做两件事:验证 + 类型

传统做法是定义 TypeScript interface,再写一套运行时校验——两套定义,两处维护,早晚不一致。

Zod 把这件事合并了:

import { z } from 'zod';

const Article = z.object({
  title: z.string().min(1, '标题不能为空').max(300),
  category: z.enum(['AI', 'Apple', '科技前沿']),
  tags: z.array(z.string().max(50)).max(10).default([]),
  status: z.enum(['published', 'draft', 'archived']).default('published'),
});

// typeof ArticleInput 自动推断为:
// { title: string; category: 'AI' | 'Apple' | '科技前沿'; tags?: string[]; status?: 'published' | 'draft' | 'archived' }
type ArticleInput = z.infer<typeof Article>;

定义 schema 的同时就得到了 TypeScript 类型。没有 interface,没有 type 别名,没有不一致的风险。.infer 是 Zod 最被低估的功能——它让你的类型定义和运行时校验永远同步。

2. .safeParse() 不抛异常

parse() 验证失败时抛出 ZodError,在 Express 中间件里需要 try/catch。.safeParse() 返回一个 discriminated union:

const result = articleCreateSchema.safeParse(req.body);

if (!result.success) {
  // result.error 是 ZodError,包含所有字段的验证失败信息
  return res.status(400).json({ errors: result.error.issues });
}

// result.data 是已校验的类型安全数据
const { title, category, tags } = result.data;

.success 就是类型守卫。if 分支一过,TypeScript 自动缩窄 result.data 的类型。不需要类型断言,不需要 as

3. 零依赖,2KB 核心

Zod 的 gzip 后核心包只有 2KB,零外部依赖。可以跑在 Node.js、浏览器、Deno、Bun 任何 JavaScript 运行时。这个大小意味着它适合任何场景——哪怕是微信小程序这种对包大小敏感的环境。

白物集实战:一个完整的 API 请求验证

白物集后端 Express API 每天接受来自多个渠道的内容入库——agent cron 写的早报、热文采集脚本批量的知识卡片、以及手动管理后台的编辑操作。每个入口的数据格式都有细微差异,但都必须符合同一个数据库约束。

我们在 api/src/lib/validation.js 里用 Zod 定义了全套 schema。以文章入库为例:

export const articleCreateSchema = z.object({
  title: z.string().min(1, '标题不能为空').max(300, '标题最长300字'),
  content_md: z.string().min(1, '内容不能为空'),
  content_html: z.string().optional().default(''),
  abstract: z.string().max(500).optional().default(''),
  category: z.enum(ALL_CATEGORIES, {
    errorMap: () => ({ message: `无效分类, 可选: ${ALL_CATEGORIES.join(', ')}` }),
  }),
  tags: z.array(z.string().max(50)).max(10).optional().default([]),
  source: z.string().max(100).optional().default('manual'),
  source_url: z.string().url('来源URL格式不正确').or(z.literal('')).optional().default(''),
  author: z.string().max(100).optional().default('白物集'),
  status: z.enum(['published', 'draft', 'archived']).optional().default('published'),
});

这个 schema 覆盖了 9 个字段,每个字段都有自己的类型、长度限制、默认值,以及自定义错误信息。如果用传统手动校验,这段代码至少 60 行——而且容易漏掉边界情况。

在 ingest 路由里使用它:

const parsed = articleCreateSchema.safeParse(req.body);
if (!parsed.success) return zodError(res, parsed.error);

const { title, content_md, content_html, abstract, category, tags, source, source_url, author, status } = parsed.data;

parsed.data 的九个字段全部有完整类型。编辑器自动补全、编译时类型检查、运行时验证——一次定义,三层保障。

踩坑记录:Zod v4 的 .issues 陷阱

2026 年 7 月,白物集 API 突然返回 500,错误是 Cannot read properties of undefined (reading 'map')

排查发现,我们的 errors.js 用了 zodErr.errors.map() 来格式化错误信息。这是 Zod v3 的 API。Zod v4 把 .errors 改成了 .issues——两者都是数组,但名字变了。zodErr.errors 返回 undefined,undefined.map() 直接崩溃。

修复很简单:zodErr.errors.map( 改为 (zodErr.issues || []).map(

这不是 Zod 的错——3.x → 4.x 的主版本号变更本身就应该预期 breaking change。但这个细节提醒我们:版本升级后,你依赖的错误处理路径也要测试。

适合的场景

任何需要接收外部数据的 Node.js 服务。 具体来说:

  • API 路由验证:Express 或 Fastify 的 request body 校验。一个 schema 覆盖所有输入字段的类型、长度、格式,比手写 if 链可靠 10 倍。
  • 配置文件校验:env 变量、yaml 配置、JSON 配置文件。Zod 的 .parse() 会在启动时校验所有配置,让错误在部署前暴露。
  • 表单数据验证:前端表单提交前的客户端校验。Zod 可以前后端共用同一个 schema。
  • 数据库记录格式保证:从 Supabase 或 Postgres 读取的数据,用 Zod 再确认一次再传给前端。

不适合的场景:

  • 已经有完善 OpenAPI 规范 + 自动化校验(如 Fastify 的 schema 验证)的项目。Zod 和 Fastify 内置验证部分重叠。
  • 纯 JavaScript 项目(无 TypeScript)。Zod 的 .infer 类型推断是最大优势,纯 JS 用不到这部分。

快速上手

第一步:安装

npm install zod

第二步:定义一个 schema

import { z } from 'zod';

const UserSchema = z.object({
  id: z.number(),
  name: z.string().min(2).max(50),
  email: z.string().email(),
  role: z.enum(['admin', 'user', 'viewer']),
  createdAt: z.string().datetime(),
});

第三步:验证数据

const input = { id: 1, name: '白物集', email: 'hello@baiwuji.top', role: 'admin', createdAt: '2026-07-13T00:00:00Z' };
const user = UserSchema.parse(input);

验证通过,user 的类型是 { id: number; name: string; email: string; role: 'admin' | 'user' | 'viewer'; createdAt: string }

第四步:在 Express 路由里使用

router.post('/users', (req, res) => {
  const result = UserSchema.safeParse(req.body);
  if (!result.success) return res.status(400).json({ errors: result.error.issues });
  // result.data 的类型安全
  res.json({ user: result.data });
});

第五步:用 .infer 导出类型

type User = z.infer<typeof UserSchema>;
// 等价于手动写 interface,但永远与 schema 同步

总结

Zod 解决了一个简单但重要的问题:让进入你程序的每一条数据都经过验证,而且不增加维护成本

它的设计哲学很克制——只做一件事(Schema 验证 + 类型推断),但做得足够好。2KB 的包体积,零依赖,前后端通用,生态里有 Zod to OpenAPI、Zod to JSON Schema 等配套工具。

白物集从第一天就在 API 层使用 Zod。9 个 route 文件,每个都依赖 validation.js 里的 schema 定义。迄今为止,因为校验逻辑本身出错导致的数据问题为零。

如果你的 TypeScript 项目还没有上 Zod,现在就是最好的时机。

← Apple 起诉 OpenAI:AI 巨头首场法律战 → Apple 频道 API 设计入门:RESTful 是什么、怎么用 →
🍎 Apple 深度分析
本文基于 Apple 公开资料及行业分析撰写。观点仅供参考与学习交流。