Zod:TypeScript 类型守卫与数据验证

我见过许多因为运行时数据不匹配而导致的崩溃,也曾写过无数防御性代码和 any 断言,哈哈 😄。TypeScript 的类型安全本来就不该止步于编译期。直到遇见 Zod,Zod 不仅是一个验证库,它为 TypeScript 带来运行时安全,是目前最优雅、最彻底的解决方案。

我们为何需要 Zod?

TypeScript 最让人上瘾的地方在于编译时类型检查,但这也是它的最大谎言,因为类型在运行时彻底消失,你需要小心小心再小心,使用 TypeScript 并不代表类型安全。

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());
});

而 Zod 的答案是:只定义一次 Schema,既得到运行时验证,又得到完美的 TypeScript 类型。

ts 复制代码
const UserSchema = z.object({
  name: z.string(),
  age: z.number(),
  role: z.enum(['admin', 'user']),
});

type User = z.infer<typeof UserSchema>;

通过 Schema 推断出完美类型 User,无需二次声明。这样, UserSchema 用于运行时验证,User 用于类型推断。

Zod 入门

bash 复制代码
npm install zod
ts 复制代码
import { z } from 'zod';

// 基础类型
const StringSchema = z.string();
const NumberSchema = z.number().int().positive();

核心 API:parse 和 safeParse,我建议优先使用 .safeParse()

ts 复制代码
const schema = z.string();

try {
  const result = schema.parse(123);
  console.log(result);
} catch (error) {
  console.error('验证失败:', error.errors);
}

parse 抛出 ZodError, 你需要通过 try catch 捕获错误,否则导致程序崩溃。

ts 复制代码
const schema = z.string();
const result = schema.safeParse(123);

if (result.success) {
  console.log('验证成功:', result.data);
} else {
  console.log('验证失败:', result.error.errors);
}

safeParse 返回 { success: true, data: T }{ success: false, error: ZodError },你可以通过 success 判断是否验证成功,然后通过 data 获取验证后的数据,或者通过 error.errors 获取错误信息, 这样你可以优雅地处理错误。

构建复杂数据模型

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'),
});

组合技巧,这些是我最常用的:

.extend() 是 Zod 最被低估的特性之一。它让你能以面向对象的方式构建 Schema 体系,比 interface 继承更安全,因为运行时验证也会继承。

ts 复制代码
// 扩展
const AdminSchema = UserSchema.extend({
  permissions: z.array(z.string()),
});

比较常见的是,实现查询接口的分页查询 Schema,分页查询 Schema 包含 page 和 pageSize 字段,自其他 Schema 可以继承分页查询 Schema 并添加其他字段。

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>;

合并, 优先级后者覆盖前者。

ts 复制代码
const MergedSchema = UserSchema.merge(z.object({
  role: z.literal('admin'), // 强制覆盖
}));

交集。

ts 复制代码
const IntersectionSchema = z.intersection(UserSchema, z.object({
  isVerified: z.boolean(),
}));

进阶模式与精细校验

可辨识联合(Discriminated Union),比如我们用它来处理 Redux Action。

可辨识联合(Discriminated Union),也称为标签联合(Tagged Union)或代数数据类型(Algebraic Data Type),是一种高级类型系统特性,用于表示可能是多种不同类型之一的值。

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>;

z.discriminatedUnion 比手动写 .or() 更清晰,TypeScript 窄化(narrowing)也更完美。

字符串高级校验

ts 复制代码
z.string()
  .min(8, '至少8位')
  .regex(/[A-Z]/, '必须含大写字母')
  .regex(/[a-z]/, '必须含小写字母')
  .regex(/[0-9]/, '必须含数字')
  .regex(/[^A-Za-z0-9]/, '必须含特殊字符');

通过 Transform 验证后自动转换,这太棒了,比如写 restful 接口时,我们希望 Query id 是 number 类型,但是传入 string 类型,我们可以借助 Transform 自动转换为 number 类型。

ts 复制代码
const IdSchema = z.string().transform(str => parseInt(str));

推断 TypeScript 类型

修改 Schema,类型自动更新,无需手动更新类型,完美同步。

ts 复制代码
type User = z.infer<typeof UserSchema>;

与 TypeScript 原生 enum 互操作。

ts 复制代码
enum Role {
  Admin = 'admin',
  User = 'user',
}

const RoleSchema = z.nativeEnum(Role);

常见应用场景

API 响应验证

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);

表单验证

与 React-Hook-Form 完美结合,类型安全 + 错误信息自动同步。AI 非常喜欢这套方案,AI 生成的代码非常不容易出错。

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),
});

但是,我们最常用的 antd 的 Form 组件,与 Zod 结合并不完美 #40580。表单一旦复杂,字段之间的关联性就更多了,如果都在 jsx 中处理,代码的可读性、可维护性就大大降低,如果把字段的定义以及验证单独提取出来,形成业务实体对应的实体逻辑,无疑更好。

社区有人为此实现了一个 antd-zod 库,是目前我比较推荐的方案。

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={[rule]}>
        <Input/>
      </Form.Item>
      <Form.Item label="Number field" name="fieldNumber" rules={[rule]}>
        <InputNumber/>
      </Form.Item>
      <Button htmlType="submit">Submit</Button>
    </Form>
  );
};

环境变量验证

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);

对于环境变量校验我推荐你使用 t3-env,使用无效的环境变量部署应用程序是一件麻烦事。这个包可以帮助你避免这种情况。它支持使用任何 Standard Schema 兼容验证器,当然包括 Zod。

定义环境变量:

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,
  },
});

使用时具有自动完成和类型推断功能

ts 复制代码
import { env } from "../env.mjs";

export const GET = (req: Request) => {
  const DATABASE_URL = env.DATABASE_URL;
  // use it...
};

服务端应用:tRPC 示例

tRPC 是一个端到端类型安全的服务端框架。tRPC 的核心魔力就在于 Zod。输入验证、类型推断、自动生成客户端类型,全程零配置。

服务端实现文章发布接口,使用 Zod 验证输入

前端类型推断

端到端类型安全对于 AI 自动补全和提升生成代码质量也是非常有帮助的

AI 生成结构化数据

许多语言模型都能够生成结构化数据,通常定义为使用"JSON modes"或"tools"。然而,您需要手动提供模式,然后验证生成的数据,因为 LLM 可能会产生错误或不完整的结构化数据。

Vercel AI SDK 通过在 generateText 上使用 output 属性,标准化了模型提供商之间的结构化对象生成。 和 streamText。您可以使用 Zod schemasValibotJSON schemas 来指定您想要的数据结构,AI 模型将生成符合该结构的数据。

使用 generateTextOutput.object() 从提示中生成结构化数据。该模式还用于验证生成的数据,确保类型安全和正确性。

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: '生成一份扬州炒饭食谱',
});

与其他验证库对比

TypeScript 支持 类型推断 体积 学习成本 生态 推荐场景
Zod 原生一流 完美 ~6KB 极强 所有 TS 项目(强烈推荐)
Yup 需 cast ~20KB 老项目、JS 项目
Joi Node.js 后端
io-ts 好(FP 风格) 喜欢函数式编程的团队
AJV 小(快) 纯 JSON 验证场景

总结

这才是 TypeScript 应该有的样子。

相关推荐
哥不是小萝莉12 小时前
OpenClaw 架构设计全解析
ai
jonjia14 小时前
模块、脚本与声明文件
typescript
jonjia14 小时前
配置 TypeScript
typescript
jonjia14 小时前
TypeScript 工具函数开发
typescript
jonjia14 小时前
注解与断言
typescript
jonjia14 小时前
IDE 超能力
typescript
jonjia14 小时前
对象类型
typescript
jonjia14 小时前
快速搭建 TypeScript 开发环境
typescript
jonjia14 小时前
TypeScript 的奇怪之处
typescript
jonjia14 小时前
类型派生
typescript