Skip to content

Zod 类型验证库详解 #36

@leno23

Description

@leno23
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Zod 类型验证库详解</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
            line-height: 1.6;
            color: #333;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            padding: 20px;
        }

        .container {
            max-width: 1200px;
            margin: 0 auto;
            background: white;
            border-radius: 12px;
            box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
            padding: 40px;
        }

        h1 {
            color: #667eea;
            font-size: 2.5em;
            margin-bottom: 10px;
            text-align: center;
        }

        .subtitle {
            text-align: center;
            color: #666;
            margin-bottom: 40px;
            font-size: 1.1em;
        }

        h2 {
            color: #764ba2;
            font-size: 1.8em;
            margin-top: 40px;
            margin-bottom: 20px;
            padding-bottom: 10px;
            border-bottom: 3px solid #667eea;
        }

        h3 {
            color: #555;
            font-size: 1.3em;
            margin-top: 30px;
            margin-bottom: 15px;
        }

        h4 {
            color: #666;
            font-size: 1.1em;
            margin-top: 20px;
            margin-bottom: 10px;
        }

        .intro {
            background: #f8f9fa;
            padding: 20px;
            border-radius: 8px;
            border-left: 4px solid #667eea;
            margin-bottom: 30px;
        }

        .comparison {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 20px;
            margin: 30px 0;
        }

        .without-zod, .with-zod {
            padding: 20px;
            border-radius: 8px;
            border: 2px solid #ddd;
        }

        .without-zod {
            background: #fff5f5;
            border-color: #fc8181;
        }

        .without-zod h3, .without-zod h4 {
            color: #c53030;
        }

        .with-zod {
            background: #f0fff4;
            border-color: #68d391;
        }

        .with-zod h3, .with-zod h4 {
            color: #22543d;
        }

        pre {
            background: #1e1e1e;
            color: #d4d4d4;
            padding: 20px;
            border-radius: 8px;
            overflow-x: auto;
            margin: 15px 0;
            font-size: 14px;
            line-height: 1.5;
        }

        code {
            font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
        }

        .code-block {
            margin: 15px 0;
        }

        .highlight {
            background: #fff3cd;
            padding: 2px 6px;
            border-radius: 3px;
            font-weight: bold;
        }

        .error {
            color: #c53030;
            font-weight: bold;
        }

        .success {
            color: #22543d;
            font-weight: bold;
        }

        .benefits {
            background: #e6f3ff;
            padding: 20px;
            border-radius: 8px;
            margin: 30px 0;
        }

        .benefits ul {
            margin-left: 20px;
            margin-top: 10px;
        }

        .benefits li {
            margin: 10px 0;
        }

        .example-section {
            margin: 30px 0;
            padding: 20px;
            background: #f8f9fa;
            border-radius: 8px;
        }

        .warning {
            background: #fff3cd;
            border-left: 4px solid #ffc107;
            padding: 15px;
            margin: 20px 0;
            border-radius: 4px;
        }

        .info {
            background: #d1ecf1;
            border-left: 4px solid #0c5460;
            padding: 15px;
            margin: 20px 0;
            border-radius: 4px;
        }

        .execution-result {
            background: #2d2d2d;
            color: #f8f8f2;
            padding: 15px;
            border-radius: 8px;
            margin: 15px 0;
            font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
            font-size: 13px;
            line-height: 1.6;
        }

        .execution-result .input {
            color: #a6e22e;
            margin-bottom: 10px;
        }

        .execution-result .output {
            color: #e6db74;
        }

        .execution-result .error-output {
            color: #f92672;
        }

        .execution-result .success-output {
            color: #66d9ef;
        }

        .execution-result .label {
            color: #75715e;
            font-size: 12px;
            margin-bottom: 5px;
        }

        .result-section {
            margin-top: 20px;
            padding-top: 20px;
            border-top: 2px dashed #ddd;
        }

        @media (max-width: 768px) {
            .comparison {
                grid-template-columns: 1fr;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>🔒 Zod 类型验证库详解</h1>
        <p class="subtitle">TypeScript 优先的模式验证库 - 使用与不使用 Zod 的对比</p>

        <div class="intro">
            <h3>什么是 Zod?</h3>
            <p>
                <strong>Zod</strong> 是一个 TypeScript 优先的模式声明和验证库。它允许你定义数据的结构(schema),
                然后自动推断 TypeScript 类型,并在运行时验证数据是否符合该结构。
            </p>
        </div>

        <h2>📊 核心对比:使用 vs 不使用 Zod</h2>

        <div class="comparison">
            <div class="without-zod">
                <h3>❌ 不使用 Zod</h3>
                <div class="code-block">
                    <pre><code>// 问题 1: 类型定义和验证分离
interface User {
  name: string;
  email: string;
  age: number;
}

// 手动编写验证函数
function validateUser(data: any): User {
  if (!data.name || typeof data.name !== 'string') {
    throw new Error('Invalid name');
  }
  if (!data.email || typeof data.email !== 'string') {
    throw new Error('Invalid email');
  }
  if (!data.age || typeof data.age !== 'number') {
    throw new Error('Invalid age');
  }
  return data as User;
}

// 问题 2: 类型不安全
function processUser(input: any) {
  // TypeScript 无法保证 input 符合 User 类型
  const user = validateUser(input);
  console.log(user.name.toUpperCase()); // 可能运行时出错
}

// 问题 3: 重复代码
// 前端需要验证,后端也需要验证
// 两个地方都要写类似的验证逻辑</code></pre>
                </div>
                <div class="result-section">
                    <h4>执行结果对比:</h4>
                    <div class="execution-result">
                        <div class="label">输入: { name: "John", email: "invalid-email", age: 25 }</div>
                        <div class="input">validateUser({ name: "John", email: "invalid-email", age: 25 })</div>
                        <div class="error-output">❌ 通过验证!(但实际上邮箱格式错误)</div>
                        <div class="label" style="margin-top: 15px;">输入: { name: "", email: "test@example.com", age: -5 }</div>
                        <div class="input">validateUser({ name: "", email: "test@example.com", age: -5 })</div>
                        <div class="error-output">❌ 通过验证!(但实际上年龄为负数)</div>
                        <div class="label" style="margin-top: 15px;">输入: { name: "John" }</div>
                        <div class="input">validateUser({ name: "John" })</div>
                        <div class="error-output">Error: Invalid email</div>
                        <div style="color: #75715e; margin-top: 10px; font-size: 12px;">
                            ⚠️ 错误信息不够详细,无法知道具体哪个字段有问题
                        </div>
                    </div>
                </div>
            </div>

            <div class="with-zod">
                <h3>✅ 使用 Zod</h3>
                <div class="code-block">
                    <pre><code>import { z } from 'zod';

// 一个 Schema 定义类型和验证规则
const UserSchema = z.object({
  name: z.string().min(1, '姓名不能为空'),
  email: z.string().email('邮箱格式不正确'),
  age: z.number().min(0).max(150),
});

// 自动推断 TypeScript 类型
type User = z.infer<typeof UserSchema>;

// 类型安全且简洁的验证
function processUser(input: unknown) {
  // Zod 自动验证并返回类型安全的结果
  const user = UserSchema.parse(input);
  console.log(user.name.toUpperCase()); // 类型安全!
}

// 可以在前端和后端共享同一个 Schema</code></pre>
                </div>
                <div class="result-section">
                    <h4>执行结果对比:</h4>
                    <div class="execution-result">
                        <div class="label">输入: { name: "John", email: "invalid-email", age: 25 }</div>
                        <div class="input">UserSchema.parse({ name: "John", email: "invalid-email", age: 25 })</div>
                        <div class="error-output">ZodError: [
  {
    "code": "invalid_string",
    "validation": "email",
    "path": ["email"],
    "message": "邮箱格式不正确"
  }
]</div>
                        <div class="label" style="margin-top: 15px;">输入: { name: "", email: "test@example.com", age: -5 }</div>
                        <div class="input">UserSchema.parse({ name: "", email: "test@example.com", age: -5 })</div>
                        <div class="error-output">ZodError: [
  {
    "code": "too_small",
    "minimum": 1,
    "path": ["name"],
    "message": "姓名不能为空"
  },
  {
    "code": "too_small",
    "minimum": 0,
    "path": ["age"],
    "message": "Number must be greater than or equal to 0"
  }
]</div>
                        <div class="label" style="margin-top: 15px;">输入: { name: "John", email: "john@example.com", age: 25 }</div>
                        <div class="input">UserSchema.parse({ name: "John", email: "john@example.com", age: 25 })</div>
                        <div class="success-output">✅ 验证通过!返回类型安全的 User 对象</div>
                        <div style="color: #75715e; margin-top: 10px; font-size: 12px;">
                            ✅ 错误信息详细,明确指出所有有问题的字段和原因
                        </div>
                    </div>
                </div>
            </div>
        </div>

        <h2>🎯 主要优势</h2>

        <div class="benefits">
            <h3>1. 类型安全</h3>
            <ul>
                <li><strong>自动类型推断</strong>:从 Schema 自动生成 TypeScript 类型</li>
                <li><strong>编译时检查</strong>:TypeScript 编译器会检查类型错误</li>
                <li><strong>运行时验证</strong>:确保数据在运行时也符合预期</li>
            </ul>

            <h3>2. 代码复用</h3>
            <ul>
                <li><strong>前后端共享</strong>:同一个 Schema 可以在前端和后端使用</li>
                <li><strong>减少重复</strong>:不需要在多个地方写相同的验证逻辑</li>
                <li><strong>单一数据源</strong>:Schema 是类型和验证的唯一来源</li>
            </ul>

            <h3>3. 更好的错误处理</h3>
            <ul>
                <li><strong>详细的错误信息</strong>:Zod 提供清晰的错误消息</li>
                <li><strong>错误定位</strong>:准确指出哪个字段有问题</li>
                <li><strong>友好的错误格式</strong>:易于展示给用户</li>
            </ul>
        </div>

        <h2>🔍 类型推断实际效果对比</h2>

        <div class="example-section">
            <div class="comparison">
                <div class="without-zod">
                    <h4>❌ 不使用 Zod - 类型推断问题</h4>
                    <pre><code>interface User {
  name: string;
  email: string;
}

function fetchUser(): User {
  // 从 API 获取数据
  return fetch('/api/user')
    .then(res => res.json()) as Promise<User>;
}

// TypeScript 认为这是 User 类型
// 但实际上运行时可能不是!
const user = await fetchUser();
console.log(user.name); // 可能报错:Cannot read property 'name' of undefined</code></pre>
                    <div class="result-section">
                        <div class="execution-result">
                            <div class="label">实际执行:</div>
                            <div class="input">const user = await fetchUser();</div>
                            <div class="output">// TypeScript 类型: User</div>
                            <div class="output">// 实际运行时值: { name: "John", email: "john@example.com" } ✅</div>
                            <div class="output">// 或者: undefined ❌</div>
                            <div class="output">// 或者: { name: "John" } ❌ (缺少 email)</div>
                            <div class="output">// 或者: null ❌</div>
                            <div class="error-output" style="margin-top: 10px;">
                                ⚠️ TypeScript 无法保证运行时类型安全
                            </div>
                        </div>
                    </div>
                </div>

                <div class="with-zod">
                    <h4>✅ 使用 Zod - 类型安全保证</h4>
                    <pre><code>import { z } from 'zod';

const UserSchema = z.object({
  name: z.string(),
  email: z.string().email(),
});

type User = z.infer<typeof UserSchema>;

async function fetchUser(): Promise<User> {
  const response = await fetch('/api/user');
  const data = await response.json();
  
  // Zod 验证并确保类型安全
  return UserSchema.parse(data);
}

// TypeScript 和运行时都保证是 User 类型
const user = await fetchUser();
console.log(user.name); // 100% 安全!</code></pre>
                    <div class="result-section">
                        <div class="execution-result">
                            <div class="label">实际执行:</div>
                            <div class="input">const user = await fetchUser();</div>
                            <div class="output">// TypeScript 类型: User ✅</div>
                            <div class="output">// 运行时验证: 通过 ✅</div>
                            <div class="output">// 实际值: { name: "John", email: "john@example.com" } ✅</div>
                            <div class="label" style="margin-top: 15px;">如果 API 返回无效数据:</div>
                            <div class="input">fetchUser() // API 返回 { name: "John" }</div>
                            <div class="error-output">ZodError: [
  {
    "code": "invalid_type",
    "expected": "string",
    "received": "undefined",
    "path": ["email"],
    "message": "Required"
  }
]
❌ 立即抛出错误,不会继续执行</div>
                            <div class="success-output" style="margin-top: 10px;">
                                ✅ 编译时和运行时双重保障
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>

        <h2>💡 实际应用场景对比</h2>

        <div class="example-section">
            <h3>场景 1: API 请求/响应验证</h3>

            <div class="comparison">
                <div class="without-zod">
                    <h4>❌ 不使用 Zod</h4>
                    <pre><code>// 前端
async function fetchUser(id: string) {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  
  // 手动验证,容易遗漏
  if (!data || !data.id || !data.name) {
    throw new Error('Invalid user data');
  }
  
  return data; // 类型不安全
}

// 后端
app.get('/api/users/:id', (req, res) => {
  const id = req.params.id;
  // 需要手动验证 id 格式
  if (!id || typeof id !== 'string') {
    return res.status(400).json({ error: 'Invalid ID' });
  }
  // ... 处理逻辑
});</code></pre>
                </div>

                <div class="with-zod">
                    <h4>✅ 使用 Zod</h4>
                    <pre><code>// shared/schema.ts (前后端共享)
import { z } from 'zod';

export const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string(),
  email: z.string().email(),
});

// 前端
import { UserSchema } from './shared/schema';

async function fetchUser(id: string) {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  
  // 自动验证并类型安全
  return UserSchema.parse(data);
}

// 后端
import { z } from 'zod';
import { UserSchema } from './shared/schema';

const IdParamSchema = z.object({
  id: z.string().uuid(),
});

app.get('/api/users/:id', (req, res) => {
  const { id } = IdParamSchema.parse(req.params);
  // id 已经是类型安全的字符串
  // ... 处理逻辑
});</code></pre>
                </div>
            </div>
        </div>

        <div class="example-section">
            <h3>场景 2: 表单验证</h3>

            <div class="comparison">
                <div class="without-zod">
                    <h4>❌ 不使用 Zod</h4>
                    <pre><code>function validateForm(data: any) {
  const errors: Record<string, string> = {};
  
  // 大量重复的验证代码
  if (!data.email) {
    errors.email = '邮箱不能为空';
  } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
    errors.email = '邮箱格式不正确';
  }
  
  if (!data.password) {
    errors.password = '密码不能为空';
  } else if (data.password.length < 8) {
    errors.password = '密码至少8位';
  }
  
  if (data.password !== data.confirmPassword) {
    errors.confirmPassword = '两次密码不一致';
  }
  
  return {
    isValid: Object.keys(errors).length === 0,
    errors,
  };
}</code></pre>
                    <div class="result-section">
                        <h4>执行结果:</h4>
                        <div class="execution-result">
                            <div class="label">输入: { email: "test", password: "123", confirmPassword: "456" }</div>
                            <div class="input">validateForm({ email: "test", password: "123", confirmPassword: "456" })</div>
                            <div class="output">{
  isValid: false,
  errors: {
    email: "邮箱格式不正确",
    password: "密码至少8位",
    confirmPassword: "两次密码不一致"
  }
}</div>
                            <div style="color: #75715e; margin-top: 10px; font-size: 12px;">
                                ⚠️ 需要手动维护验证逻辑,容易遗漏边界情况
                            </div>
                        </div>
                    </div>
                </div>

                <div class="with-zod">
                    <h4>✅ 使用 Zod</h4>
                    <pre><code>import { z } from 'zod';

const FormSchema = z.object({
  email: z.string().email('邮箱格式不正确'),
  password: z.string().min(8, '密码至少8位'),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: '两次密码不一致',
  path: ['confirmPassword'],
});

function validateForm(data: unknown) {
  const result = FormSchema.safeParse(data);
  
  if (result.success) {
    return { isValid: true, errors: {} };
  } else {
    // Zod 自动格式化错误
    const errors: Record<string, string> = {};
    result.error.errors.forEach((err) => {
      if (err.path.length > 0) {
        errors[err.path[0] as string] = err.message;
      }
    });
    return { isValid: false, errors };
  }
}</code></pre>
                    <div class="result-section">
                        <h4>执行结果:</h4>
                        <div class="execution-result">
                            <div class="label">输入: { email: "test", password: "123", confirmPassword: "456" }</div>
                            <div class="input">validateForm({ email: "test", password: "123", confirmPassword: "456" })</div>
                            <div class="output">{
  isValid: false,
  errors: {
    email: "邮箱格式不正确",
    password: "密码至少8位",
    confirmPassword: "两次密码不一致"
  }
}</div>
                            <div class="label" style="margin-top: 15px;">输入: { email: "user@example.com", password: "password123", confirmPassword: "password123" }</div>
                            <div class="input">validateForm({ email: "user@example.com", password: "password123", confirmPassword: "password123" })</div>
                            <div class="success-output">{
  isValid: true,
  errors: {}
}</div>
                            <div style="color: #75715e; margin-top: 10px; font-size: 12px;">
                                ✅ Schema 集中管理,自动处理所有验证规则
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>

        <div class="example-section">
            <h3>场景 3: 环境变量验证</h3>

            <div class="comparison">
                <div class="without-zod">
                    <h4>❌ 不使用 Zod</h4>
                    <pre><code>// 容易出错,类型不安全
const config = {
  apiUrl: process.env.API_URL || 'http://localhost:3000',
  port: parseInt(process.env.PORT || '3000', 10),
  dbUrl: process.env.DATABASE_URL,
};

// 运行时才发现环境变量缺失或格式错误
if (!config.dbUrl) {
  throw new Error('DATABASE_URL is required');
}</code></pre>
                    <div class="result-section">
                        <h4>执行结果:</h4>
                        <div class="execution-result">
                            <div class="label">情况 1: DATABASE_URL 未设置</div>
                            <div class="input">config.dbUrl = undefined</div>
                            <div class="error-output">❌ 应用启动成功,但在运行时才报错</div>
                            <div class="label" style="margin-top: 15px;">情况 2: PORT = "abc"</div>
                            <div class="input">parseInt("abc", 10) = NaN</div>
                            <div class="error-output">❌ config.port = NaN (类型错误但不会报错)</div>
                            <div class="label" style="margin-top: 15px;">情况 3: API_URL = "not-a-url"</div>
                            <div class="input">config.apiUrl = "not-a-url"</div>
                            <div class="error-output">❌ 无效的 URL,但不会立即发现</div>
                            <div style="color: #75715e; margin-top: 10px; font-size: 12px;">
                                ⚠️ 问题在运行时才暴露,调试困难
                            </div>
                        </div>
                    </div>
                </div>

                <div class="with-zod">
                    <h4>✅ 使用 Zod</h4>
                    <pre><code>import { z } from 'zod';

const ConfigSchema = z.object({
  apiUrl: z.string().url(),
  port: z.coerce.number().int().positive(),
  dbUrl: z.string().url(),
});

// 启动时立即验证,类型安全
const config = ConfigSchema.parse({
  apiUrl: process.env.API_URL,
  port: process.env.PORT,
  dbUrl: process.env.DATABASE_URL,
});

// config 现在是类型安全的配置对象</code></pre>
                    <div class="result-section">
                        <h4>执行结果:</h4>
                        <div class="execution-result">
                            <div class="label">情况 1: DATABASE_URL 未设置</div>
                            <div class="input">ConfigSchema.parse({ apiUrl: "...", port: "3000", dbUrl: undefined })</div>
                            <div class="error-output">ZodError: [
  {
    "code": "invalid_type",
    "expected": "string",
    "received": "undefined",
    "path": ["dbUrl"],
    "message": "Required"
  }
]
❌ 应用启动时立即失败,错误信息清晰</div>
                            <div class="label" style="margin-top: 15px;">情况 2: PORT = "abc"</div>
                            <div class="input">ConfigSchema.parse({ port: "abc", ... })</div>
                            <div class="error-output">ZodError: [
  {
    "code": "invalid_type",
    "expected": "number",
    "received": "nan",
    "path": ["port"],
    "message": "Expected number, received nan"
  }
]
❌ 立即发现类型错误</div>
                            <div class="label" style="margin-top: 15px;">情况 3: 所有环境变量正确</div>
                            <div class="input">ConfigSchema.parse({ 
  apiUrl: "https://api.example.com",
  port: "3000",
  dbUrl: "postgres://localhost/db"
})</div>
                            <div class="success-output">✅ 验证通过!
config = {
  apiUrl: "https://api.example.com",  // string (类型安全)
  port: 3000,                          // number (自动转换)
  dbUrl: "postgres://localhost/db"     // string (类型安全)
}</div>
                            <div style="color: #75715e; margin-top: 10px; font-size: 12px;">
                                ✅ 启动时立即验证,类型完全安全
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>

        <h2>⚠️ 常见问题</h2>

        <div class="warning">
            <h3>Q: Zod 会增加包体积吗?</h3>
            <p>
                A: Zod 是一个轻量级库(约 10KB gzipped),相比手动编写验证代码,
                它实际上可能减少总体代码量,特别是在需要复杂验证的场景中。
            </p>
        </div>

        <div class="info">
            <h3>Q: 性能如何?</h3>
            <p>
                A: Zod 的性能非常好,对于大多数应用场景来说,验证开销可以忽略不计。
                只有在处理大量数据(如数万条记录)时才需要考虑性能优化。
            </p>
        </div>

        <div class="warning">
            <h3>Q: 学习曲线陡峭吗?</h3>
            <p>
                A: Zod 的 API 设计非常直观,如果你熟悉 TypeScript,学习 Zod 只需要几分钟。
                官方文档非常完善,提供了大量示例。
            </p>
        </div>

        <h2>📚 总结</h2>

        <div class="benefits">
            <p><strong>使用 Zod 的核心价值:</strong></p>
            <ul>
                <li><strong>类型安全</strong>:编译时和运行时双重保障</li>
                <li><strong>代码复用</strong>:前后端共享 Schema,减少重复代码</li>
                <li><strong>更好的 DX</strong>:开发体验更好,错误更早发现</li>
                <li><strong>可维护性</strong>:单一数据源,易于维护和更新</li>
                <li><strong>类型推断</strong>:自动从 Schema 生成 TypeScript 类型</li>
            </ul>

            <p style="margin-top: 20px;">
                <strong>不使用 Zod 的问题:</strong>
            </p>
            <ul>
                <li>❌ 类型定义和验证逻辑分离,容易不同步</li>
                <li>❌ 需要手动编写大量验证代码</li>
                <li>❌ 前后端验证逻辑重复,维护成本高</li>
                <li>❌ 错误信息不够友好,调试困难</li>
                <li>❌ 运行时类型不安全,容易出现运行时错误</li>
            </ul>
        </div>

        <div style="text-align: center; margin-top: 40px; padding-top: 20px; border-top: 2px solid #eee; color: #666;">
            <p>💡 <strong>建议</strong>:在 TypeScript 项目中使用 Zod 可以显著提升代码质量和开发效率</p>
        </div>
    </div>
</body>
</html>

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions