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 }
});
知识回顾
TypeScript 的静态局限性
- 类型检查仅在编译时生效,运行时数据可能因外部输入导致类型错误。
- 需结合运行时库防御动态数据风险。
zod 核心能力
- 通过
Schema
定义验证规则,支持复杂类型与类型推断。 - 提供详细的错误信息和类型转换功能。
- 通过
valibot 的优势
- 函数式 API 设计,零类型注解需求,体积更小。
- 适合对性能敏感或非 TypeScript 项目。
JSON 输入防护
- 对所有外部数据(API、存储)进行运行时验证,避免类型断层。
- 与静态类型系统结合使用,构建多层防御体系。
课后练习
(单选)以下哪种说法正确?
- A. TypeScript 的类型检查在运行时仍会生效。
- B. zod 验证失败时会直接抛出类型安全的错误对象。
- C. valibot 需要依赖 TypeScript 类型注解。
- D. 运行时验证仅用于前端,后端无需处理。
(编码)使用 zod 定义以下接口的验证模式:
typescriptinterface Product { id: string; // UUID 格式 price: number; // 必须 ≥ 0.01 tags: string[]; // 每个标签长度 ≤ 20 }
typescriptconst productSchema = z.object({ id: z.string()._____, price: z.number()._____, tags: z.array(_____) });
(填空)valibot 中的
union
方法用于定义____类型,例如:typescriptconst paymentMethod = union([literal("credit_card"), literal("paypal")]); // 这表示字段只能是____或____。