Zod:TypeScript 类型守卫与数据验证
<p>我见过许多因为运行时数据不匹配而导致的崩溃,也曾写过无数防御性代码和 <code>any</code> 断言,哈哈 😄。TypeScript 的类型安全本来就不该止步于编译期。直到遇见 Zod,Zod 不仅是一个验证库,它为 TypeScript 带来运行时安全,是目前最优雅、最彻底的解决方案。</p><h2 id="我们为何需要-zod">我们为何需要 Zod?</h2>
<p>TypeScript 最让人上瘾的地方在于编译时类型检查,但这也是它的最大谎言,因为类型在运行时彻底消失,你需要小心小心再小心,使用 TypeScript 并不代表类型安全。</p>
<pre><code class="language-ts">interface User {
name: string;
age: number;
role: 'admin' | 'user';
}
fetch('/api/user').then(res => res.json()).then((data: User) => {
// 编译通过,但如果 data.role 返回的是 "administrator",运行会报错
console.log(data.role.toUpperCase());
});
</code></pre>
<p>而 Zod 的答案是:只定义一次 Schema,既得到运行时验证,又得到完美的 TypeScript 类型。</p>
<pre><code class="language-ts">const UserSchema = z.object({
name: z.string(),
age: z.number(),
role: z.enum(['admin', 'user']),
});
type User = z.infer<typeof UserSchema>;
</code></pre>
<p>通过 Schema 推断出完美类型 User,无需二次声明。这样, UserSchema 用于运行时验证,User 用于类型推断。</p>
<h2 id="zod-入门">Zod 入门</h2>
<pre><code class="language-bash">npm install zod
</code></pre>
<pre><code class="language-ts">import { z } from 'zod';
// 基础类型
const StringSchema = z.string();
const NumberSchema = z.number().int().positive();
</code></pre>
<p>核心 API:parse 和 safeParse,我建议优先使用 <code>.safeParse()</code>。</p>
<pre><code class="language-ts">const schema = z.string();
try {
const result = schema.parse(123);
console.log(result);
} catch (error) {
console.error('验证失败:', error.errors);
}
</code></pre>
<p>parse 抛出 ZodError, 你需要通过 <code>try catch</code> 捕获错误,否则导致程序崩溃。</p>
<pre><code class="language-ts">const schema = z.string();
const result = schema.safeParse(123);
if (result.success) {
console.log('验证成功:', result.data);
} else {
console.log('验证失败:', result.error.errors);
}
</code></pre>
<p>safeParse 返回 <code>{ success: true, data: T }</code> 或 <code>{ success: false, error: ZodError }</code>,你可以通过 success 判断是否验证成功,然后通过 data 获取验证后的数据,或者通过 <code>error.errors</code> 获取错误信息, 这样你可以优雅地处理错误。</p>
<h2 id="构建复杂数据模型">构建复杂数据模型</h2>
<pre><code class="language-ts">const AddressSchema = z.object({
street: z.string(),
city: z.string(),
zipCode: z.string().regex(/^\d{5}$/),
});
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(2).max(50),
email: z.string().email(),
age: z.number().int().min(13).max(120),
address: AddressSchema.optional(), // 可选嵌套对象
tags: z.array(z.string()).default([]), // 默认值
role: z.enum(['admin', 'user', 'moderator']),
status: z.enum(['active', 'inactive']).default('active'),
});
</code></pre>
<p>组合技巧,这些是我最常用的:</p>
<p><code>.extend()</code> 是 Zod 最被低估的特性之一。它让你能以面向对象的方式构建 Schema 体系,比 interface 继承更安全,因为运行时验证也会继承。</p>
<pre><code class="language-ts">// 扩展
const AdminSchema = UserSchema.extend({
permissions: z.array(z.string()),
});
</code></pre>
<p>比较常见的是,实现查询接口的分页查询 Schema,分页查询 Schema 包含 page 和 pageSize 字段,自其他 Schema 可以继承分页查询 Schema 并添加其他字段。</p>
<pre><code class="language-ts">const PageSchema = z.object({
page: z.number().min(1).default(1),
pageSize: z.number().min(1).max(100).default(10),
});
const UserPageSchema = PageSchema.extend({
name: z.string().min(2).max(50),
});
type UserPage = z.infer<typeof UserPageSchema>;
</code></pre>
<p>合并, 优先级后者覆盖前者。</p>
<pre><code class="language-ts">const MergedSchema = UserSchema.merge(z.object({
role: z.literal('admin'), // 强制覆盖
}));
</code></pre>
<p>交集。</p>
<pre><code class="language-ts">const IntersectionSchema = z.intersection(UserSchema, z.object({
isVerified: z.boolean(),
}));
</code></pre>
<h2 id="进阶模式与精细校验">进阶模式与精细校验</h2>
<p>可辨识联合(Discriminated Union),比如我们用它来处理 Redux Action。</p>
<blockquote>
<p>可辨识联合(Discriminated Union),也称为标签联合(Tagged Union)或代数数据类型(Algebraic Data Type),是一种高级类型系统特性,用于表示可能是多种不同类型之一的值。</p>
</blockquote>
<pre><code class="language-ts">const ActionSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('INCREMENT'), payload: z.number() }),
z.object({ type: z.literal('DECREMENT'), payload: z.number() }),
z.object({ type: z.literal('SET_USER'), payload: UserSchema }),
]);
type Action = z.infer<typeof ActionSchema>;
</code></pre>
<p><code>z.discriminatedUnion</code> 比手动写 <code>.or()</code> 更清晰,TypeScript 窄化(narrowing)也更完美。</p>
<p>字符串高级校验</p>
<pre><code class="language-ts">z.string()
.min(8, '至少8位')
.regex(//, '必须含大写字母')
.regex(//, '必须含小写字母')
.regex(//, '必须含数字')
.regex(/[^A-Za-z0-9]/, '必须含特殊字符');
</code></pre>
<p>通过 Transform 验证后自动转换,这太棒了,比如写 restful 接口时,我们希望 Query id 是 number 类型,但是传入 string 类型,我们可以借助 Transform 自动转换为 number 类型。</p>
<pre><code class="language-ts">const IdSchema = z.string().transform(str => parseInt(str));
</code></pre>
<h2 id="推断-typescript-类型">推断 TypeScript 类型</h2>
<p>修改 Schema,类型自动更新,无需手动更新类型,完美同步。</p>
<pre><code class="language-ts">type User = z.infer<typeof UserSchema>;
</code></pre>
<p>与 TypeScript 原生 enum 互操作。</p>
<pre><code class="language-ts">enum Role {
Admin = 'admin',
User = 'user',
}
const RoleSchema = z.nativeEnum(Role);
</code></pre>
<h2 id="常见应用场景">常见应用场景</h2>
<h3 id="api-响应验证">API 响应验证</h3>
<pre><code class="language-ts">async function apiFetch<T extends z.ZodType>(
url: string,
schema: T
): Promise<z.infer<T>> {
const res = await fetch(url);
const data = await res.json();
const result = schema.safeParse(data);
if (!result.success) {
throw new Error(`API验证失败: ${result.error.message}`);
}
return result.data;
}
// 使用
const user = await apiFetch('/api/user', UserSchema);
</code></pre>
<h3 id="表单验证">表单验证</h3>
<p>与 React-Hook-Form 完美结合,类型安全 + 错误信息自动同步。AI 非常喜欢这套方案,AI 生成的代码非常不容易出错。</p>
<pre><code class="language-ts">const formSchema = z.object({
email: z.string().email('邮箱格式错误'),
password: z.string().min(8, '密码至少8位'),
confirm: z.string(),
}).refine(data => data.password === data.confirm, {
message: "两次密码不一致",
path: ["confirm"],
});
type FormData = z.infer<typeof formSchema>;
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(formSchema),
});
</code></pre>
<p>但是,我们最常用的 antd 的 Form 组件,与 Zod 结合并不完美 #40580。表单一旦复杂,字段之间的关联性就更多了,如果都在 jsx 中处理,代码的可读性、可维护性就大大降低,如果把字段的定义以及验证单独提取出来,形成业务实体对应的实体逻辑,无疑更好。</p>
<p>社区有人为此实现了一个 antd-zod 库,是目前我比较推荐的方案。</p>
<pre><code class="language-tsx">import { createSchemaFieldRule } from 'antd-zod';
const CustomFormValidationSchema = z.object({
fieldString: z.string(),
fieldNumber: z.number(),
});
const rule = createSchemaFieldRule(CustomFormValidationSchema);
export function SimpleForm() {
return (
<Form>
<Form.Item label="String field" name="fieldString" rules={}>
<Input/>
</Form.Item>
<Form.Item label="Number field" name="fieldNumber" rules={}>
<InputNumber/>
</Form.Item>
<Button htmlType="submit">Submit</Button>
</Form>
);
};
</code></pre>
<h3 id="环境变量验证">环境变量验证</h3>
<pre><code class="language-ts">const envSchema = z.object({
DATABASE_URL: z.string().url(),
NODE_ENV: z.enum(['development', 'production', 'test']),
JWT_SECRET: z.string().min(32),
});
type Env = z.infer<typeof envSchema>;
export const env = envSchema.parse(process.env);
</code></pre>
<p>对于环境变量校验我推荐你使用 t3-env,使用无效的环境变量部署应用程序是一件麻烦事。这个包可以帮助你避免这种情况。它支持使用任何Standard Schema 兼容验证器,当然包括 Zod。</p>
<p>定义环境变量:</p>
<pre><code class="language-ts">// src/env.mjs
import { createEnv } from "@t3-oss/env-nextjs"; // or core package
import { z } from "zod";
export const env = createEnv({
/*
* Serverside Environment variables, not available on the client.
* Will throw if you access these variables on the client.
*/
server: {
DATABASE_URL: z.string().url(),
OPEN_AI_API_KEY: z.string().min(1),
},
/*
* Environment variables available on the client (and server).
*
* 💡 You'll get type errors if these are not prefixed with NEXT_PUBLIC_.
*/
client: {
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().min(1),
},
/*
* Due to how Next.js bundles environment variables on Edge and Client,
* we need to manually destructure them to make sure all are included in bundle.
*
* 💡 You'll get type errors if not all variables from `server` & `client` are included here.
*/
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
OPEN_AI_API_KEY: process.env.OPEN_AI_API_KEY,
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY:
process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
},
});
</code></pre>
<p>使用时具有自动完成和类型推断功能</p>
<pre><code class="language-ts">import { env } from "../env.mjs";
export const GET = (req: Request) => {
const DATABASE_URL = env.DATABASE_URL;
// use it...
};
</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/1501373/202512/1501373-20251215092951646-947278288.png" alt="image" width="913" height="276" loading="lazy"></p>
<h3 id="服务端应用trpc-示例">服务端应用:tRPC 示例</h3>
<p>tRPC 是一个端到端类型安全的服务端框架。tRPC 的核心魔力就在于 Zod。输入验证、类型推断、自动生成客户端类型,全程零配置。</p>
<p>服务端实现文章发布接口,使用 Zod 验证输入</p>
<p><img src="https://img2024.cnblogs.com/blog/1501373/202601/1501373-20260104094555995-1536803202.png" alt="image" width="754" height="471" loading="lazy"></p>
<p>前端类型推断</p>
<p><img src="https://img2024.cnblogs.com/blog/1501373/202601/1501373-20260104095551745-800796286.png" alt="image" width="664" height="204" loading="lazy"></p>
<p>端到端类型安全对于 AI 自动补全和提升生成代码质量也是非常有帮助的</p>
<p><img src="https://img2024.cnblogs.com/blog/1501373/202601/1501373-20260104095742062-1513470318.png" alt="image" width="589" height="209" loading="lazy"></p>
<h3 id="ai-生成结构化数据">AI 生成结构化数据</h3>
<p>许多语言模型都能够生成结构化数据,通常定义为使用“JSON modes”或“tools”。然而,您需要手动提供模式,然后验证生成的数据,因为 LLM 可能会产生错误或不完整的结构化数据。</p>
<p>Vercel AI SDK 通过在 <code>generateText</code> 上使用 <code>output</code> 属性,标准化了模型提供商之间的结构化对象生成。 和 <code>streamText</code>。您可以使用 Zod schemas,Valibot 或 JSON schemas 来指定您想要的数据结构,AI 模型将生成符合该结构的数据。</p>
<p>使用 <code>generateText</code> 和 <code>Output.object()</code> 从提示中生成结构化数据。该模式还用于验证生成的数据,确保类型安全和正确性。</p>
<pre><code class="language-ts">import { generateText, Output } from 'ai';
import { deepseek } from "@ai-sdk/deepseek";
import { z } from 'zod';
const { output } = await generateText({
model: deepseek("deepseek-v3.1"),
output: Output.object({
schema: z.object({
recipe: z.object({
name: z.string(),
ingredients: z.array(
z.object({ name: z.string(), amount: z.string() }),
),
steps: z.array(z.string()),
}),
}),
}),
prompt: '生成一份扬州炒饭食谱',
});
</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/1501373/202601/1501373-20260104102825415-1010215762.png" alt="image" width="860" height="366" loading="lazy"></p>
<h2 id="与其他验证库对比">与其他验证库对比</h2>
<table>
<thead>
<tr>
<th>库</th>
<th>TypeScript 支持</th>
<th>类型推断</th>
<th>体积</th>
<th>学习成本</th>
<th>生态</th>
<th>推荐场景</th>
</tr>
</thead>
<tbody>
<tr>
<td>Zod</td>
<td>原生一流</td>
<td>完美</td>
<td>~6KB</td>
<td>低</td>
<td>极强</td>
<td>所有 TS 项目(强烈推荐)</td>
</tr>
<tr>
<td>Yup</td>
<td>需 cast</td>
<td>差</td>
<td>~20KB</td>
<td>中</td>
<td>强</td>
<td>老项目、JS 项目</td>
</tr>
<tr>
<td>Joi</td>
<td>差</td>
<td>无</td>
<td>大</td>
<td>中</td>
<td>强</td>
<td>Node.js 后端</td>
</tr>
<tr>
<td>io-ts</td>
<td>好(FP 风格)</td>
<td>好</td>
<td>中</td>
<td>高</td>
<td>弱</td>
<td>喜欢函数式编程的团队</td>
</tr>
<tr>
<td>AJV</td>
<td>差</td>
<td>无</td>
<td>小(快)</td>
<td>中</td>
<td>强</td>
<td>纯 JSON 验证场景</td>
</tr>
</tbody>
</table>
<h2 id="总结">总结</h2>
<p>这才是 TypeScript 应该有的样子。</p><br><br>
来源:https://www.cnblogs.com/guangzan/p/19350726
頁:
[1]