Skip to content

D24. 使用 JavaScript 进行运行时类型验证

4.1. 🌟 TypeScript 的静态类型局限与运行时验证必要性

4.1.1. 静态类型与运行时的鸿沟

核心思想: TypeScript 的类型检查仅在编译时生效,运行时数据可能因外部输入或动态操作破坏类型安全

示例:TypeScript 的静态类型失效场景

typescript
class People {
  name: string;
  age: number;

  constructor(people: string) {
    const parsed = JSON.parse(people);
    this.name = parsed.name;
    this.age = parsed.age;
  }
}

// 编译通过,但运行时出错
const person = new People("{ name: 123, age: 12 }"); // name 被设置为了数字

问题根源

  • 动态输入:用户输入、第三方 API、文件读取等外部数据可能违反类型定义。
  • 类型擦除:TypeScript 编译后生成的 JavaScript 代码不保留类型信息。

4.1.2. 现实场景中的类型漏洞示例

场景 1:用户表单提交

typescript
// 表单数据可能包含非数字的 age
interface UserForm {
  name: string;
  age: number;
}

function processForm(data: UserForm) {
  console.log(`Age is ${data.age * 2}`); // 若 age 是字符串,会隐式转换导致错误
}

// 假设前端提交的数据:{ name: "Alice", age: "twenty" }
processForm({ name: "Alice", age: "twenty" }); // 运行时计算失败

场景 2:第三方 API 数据

typescript
interface ApiResponse {
  id: number;
  content: string[];
}

// API 返回的数据可能包含未定义的字段或类型错误
const response = await fetch("/api/data").then(res => res.json());
// 若 response.content 是字符串而非数组,后续代码会崩溃
response.content.forEach(item => console.log(item));

4.2. 🌟 zod:现代运行时类型验证标准库

4.2.1. zod 核心概念:模式(Schema)

核心思想: 通过定义 zod.Schema 描述数据结构,实现运行时校验与类型转换

安装与基础用法

bash
npm install zod

定义对象验证模式

typescript
import { z } from "zod";

const userSchema = z.object({
  name: z.string().min(3),
  age: z.number().int().positive(),
  isAdmin: z.boolean().optional(),
});

// 验证数据并转换为类型安全的值
try {
  const data = userSchema.parse({
    name: "John",
    age: "25", // ❌ 类型错误:字符串转数字失败
  });
} catch (err) {
  console.error((err as z.ZodError).errors); // 输出详细错误信息
}

4.2.2. 基础验证:对象、数组、自定义类型

验证嵌套对象

typescript
const addressSchema = z.object({
  street: z.string(),
  city: z.string(),
});

const userSchema = z.object({
  name: z.string(),
  addresses: z.array(addressSchema).min(1),
});

自定义联合类型

typescript
const paymentMethodSchema = z.union([
  z.literal("credit_card"),
  z.literal("paypal"),
]);

类型推断

typescript
type User = z.infer<typeof userSchema>; // 自动推导类型

4.2.3. 错误处理与类型转换

优雅的错误处理

typescript
function validateUser(data: any) {
  try {
    return userSchema.parse(data);
  } catch (err) {
    const errors = (err as z.ZodError).flatten().fieldErrors;
    throw new Error(`Validation failed: ${JSON.stringify(errors)}`);
  }
}

类型转换(Coercion)

typescript
const numberSchema = z.string().transform(str => parseInt(str, 10));
numberSchema.parse("42"); // 成功,返回数字 42

4.3. 🌟 valibot:轻量级高性能验证方案

4.3.1. valibot 的核心优势:零类型注解

核心思想: 通过纯函数式 API 定义验证规则,无需 TypeScript 类型注解,验证器可独立于类型系统存在。

安装与基础用法

bash
npm install valibot

定义验证规则

typescript
import { object, string, number, optional } from "valibot";

const userValidator = object({
  name: string(),
  age: number(),
  isAdmin: optional(number()), // 允许省略或数字
});

// 验证数据
const result = validate(userValidator, { name: "Alice", age: 25 });
// result: { valid: true, value: { name: "Alice", age: 25 } }

4.3.2. 与 zod 的对比:API 简洁性与性能

API 对比

typescript
// zod
z.object({ name: z.string().min(3) });

// valibot
object({ name: string().min(3) }); // 更简洁的链式调用

性能优势

  • 零类型注解:验证器与 TypeScript 类型解耦,适合非 TypeScript 项目。
  • 更小体积:valibot 的压缩后体积约 1KB,zod 约 8KB

4.3.3. 完整验证流程示例

验证复杂结构

typescript
import {
  object,
  array,
  string,
  number,
  literal,
  union
} from "valibot";

const paymentMethod = union([literal("credit_card"), literal("paypal")]);

const orderValidator = object({
  id: string().length(36), // UUID 校验
  items: array(
    object({
      productId: string(),
      quantity: number().min(1),
    })
  ),
  paymentMethod: paymentMethod,
});

// 验证并获取安全数据
const order = validate(orderValidator, rawOrderData);
if (order.valid) {
  processOrder(order.value);
} else {
  console.error(order.errors);
}

4.4. 🌟 JSON 输入的类型安全防护

4.4.1. 前后端数据交互的类型断层

核心问题: 即使后端严格验证数据,前端仍需二次验证以防御:

  • 网络劫持导致的数据篡改
  • 不同前端客户端的兼容性问题
  • 开发阶段的 API 版本差异

4.4.2. 验证 JSON 数据的典型场景

场景 1:解析 HTTP 响应

typescript
// 使用 zod 验证 API 响应
const apiResponseSchema = z.object({
  status: z.number(),
  data: z.array(z.object({ id: z.string() })),
});

try {
  const response = await fetch("/api/data");
  const data = apiResponseSchema.parse(await response.json());
  // 安全使用 data
} catch (err) {
  // 处理验证失败
}

场景 2:读取本地存储数据

typescript
// 使用 valibot 验证 localStorage 数据
const storedData = localStorage.getItem("user");
const userValidator = object({ name: string(), age: number() });

if (storedData) {
  const result = validate(userValidator, JSON.parse(storedData));
  if (result.valid) {
    // 安全恢复用户对象
  }
}

4.4.3. 与 REST API 结合的最佳实践

完整流程:从定义到验证

typescript
// 1. 定义 API 响应模式(zod)
const postSchema = z.object({
  id: z.string(),
  title: z.string(),
  content: z.string(),
  comments: z.array(
    z.object({
      author: z.string(),
      text: z.string(),
    })
  ),
});

// 2. 封装请求函数
async function fetchPost(id: string) {
  const response = await fetch(`/api/posts/${id}`);
  const raw = await response.json();
  return postSchema.parse(raw); // 强制类型安全
}

// 3. 安全使用数据
const post = await fetchPost("123");
post.comments.forEach(comment => {
  // 类型推断为 { author: string, text: string }
});

知识回顾

  1. TypeScript 的静态局限性

    • 类型检查仅在编译时生效,运行时数据可能因外部输入导致类型错误。
    • 需结合运行时库防御动态数据风险。
  2. zod 核心能力

    • 通过 Schema 定义验证规则,支持复杂类型与类型推断。
    • 提供详细的错误信息和类型转换功能。
  3. valibot 的优势

    • 函数式 API 设计,零类型注解需求,体积更小。
    • 适合对性能敏感或非 TypeScript 项目。
  4. JSON 输入防护

    • 对所有外部数据(API、存储)进行运行时验证,避免类型断层。
    • 与静态类型系统结合使用,构建多层防御体系。

课后练习

  1. (单选)以下哪种说法正确?

    • A. TypeScript 的类型检查在运行时仍会生效。
    • B. zod 验证失败时会直接抛出类型安全的错误对象。
    • C. valibot 需要依赖 TypeScript 类型注解。
    • D. 运行时验证仅用于前端,后端无需处理。
  2. (编码)使用 zod 定义以下接口的验证模式:

    typescript
    interface Product {
      id: string; // UUID 格式
      price: number; // 必须 ≥ 0.01
      tags: string[]; // 每个标签长度 ≤ 20
    }
    typescript
    const productSchema = z.object({
      id: z.string()._____,
      price: z.number()._____,
      tags: z.array(_____)
    });
  3. (填空)valibot 中的 union 方法用于定义____类型,例如:

    typescript
    const paymentMethod = union([literal("credit_card"), literal("paypal")]);
    // 这表示字段只能是____或____。

扩展阅读

Built by Vitepress | Apache 2.0 Licensed