各位观众老爷们,晚上好!我是今晚的讲师,人称 Bug 终结者(虽然我制造的 Bug 也不少)。今天咱们聊聊 TypeScript 世界里的一位重量级嘉宾:Zod。
TypeScript 静态类型检查很棒,但它有个小瑕疵:只在编译时生效。一旦代码跑起来,类型就靠不住了,外部数据(比如 API 返回的数据、用户输入的数据)就像脱缰的野马,类型可能千奇百怪。这时候,Zod 就闪亮登场了,它能在运行时进行类型校验,让你的代码更健壮、更安心。
一、Zod 是个啥?为啥要用它?
简单来说,Zod 是一个 TypeScript 优先的声明和验证库。它允许你用简洁的代码定义数据的形状(schema),然后在运行时校验数据是否符合这个形状。
为啥要用它呢?
- 运行时类型安全: 确保你接收到的数据符合预期,防止运行时错误。
- 数据清洗和转换: Zod 不仅校验数据,还能帮你清洗和转换数据,比如把字符串转换成数字,或者把日期字符串转换成 Date 对象。
- 类型推断: 从 Zod schema 中自动推断 TypeScript 类型,减少手动编写类型定义的工作。
- 清晰的错误信息: Zod 提供详细的错误信息,方便你定位问题。
- 与 TypeScript 无缝集成: Zod 本身就是用 TypeScript 写的,与 TypeScript 项目配合默契。
二、Zod 的核心概念:Schema
Schema 是 Zod 的核心,它定义了数据的形状。你可以用 Zod 提供的各种 schema 类型来描述你的数据结构。
让我们从一些基本类型开始:
Schema 类型 | 描述 | 示例 |
---|---|---|
z.string() |
字符串 | z.string().min(1).max(255) |
z.number() |
数字 | z.number().int().positive() |
z.boolean() |
布尔值 | z.boolean() |
z.date() |
日期 | z.date() |
z.null() |
null 值 |
z.null() |
z.undefined() |
undefined 值 |
z.undefined() |
z.any() |
任何类型(慎用) | z.any() |
z.unknown() |
任何类型,但使用前需要进行类型断言 | z.unknown() |
z.literal() |
具有特定值的类型 | z.literal("hello") |
z.enum() |
枚举类型 | z.enum(["red", "green", "blue"]) |
z.array() |
数组 | z.array(z.string()) |
z.object() |
对象 | z.object({ name: z.string(), age: z.number() }) |
z.union() |
联合类型 | z.union([z.string(), z.number()]) |
z.intersection() |
交叉类型 | z.intersection([z.object({a: z.string()}), z.object({b: z.number()})]) |
z.tuple() |
元组 | z.tuple([z.string(), z.number()]) |
z.record() |
记录(键值对,键为字符串) | z.record(z.number()) |
z.map() |
Map 对象 | z.map(z.string(), z.number()) |
z.set() |
Set 对象 | z.set(z.string()) |
代码示例:
import { z } from "zod";
// 定义一个字符串 schema,要求长度在 1 到 255 之间
const nameSchema = z.string().min(1).max(255);
// 定义一个数字 schema,要求是整数且大于 0
const ageSchema = z.number().int().positive();
// 定义一个日期 schema
const birthdaySchema = z.date();
// 定义一个包含 name, age, birthday 字段的对象 schema
const personSchema = z.object({
name: nameSchema,
age: ageSchema,
birthday: birthdaySchema,
email: z.string().email().optional() // 可选的 email 字段
});
// 定义一个联合类型 schema,可以是字符串或数字
const idSchema = z.union([z.string(), z.number()]);
// 定义一个数组 schema,数组元素都是字符串
const stringArraySchema = z.array(z.string());
// 定义一个枚举类型 schema
const colorSchema = z.enum(["red", "green", "blue"]);
// 使用 schema 进行校验
const validPerson = { name: "Alice", age: 30, birthday: new Date() };
const invalidPerson = { name: "", age: -1, birthday: "not a date" };
try {
const parsedPerson = personSchema.parse(validPerson); // 校验成功,返回解析后的数据
console.log("Valid person:", parsedPerson);
} catch (error) {
console.error("Invalid person:", error); // 校验失败,抛出 ZodError
}
try {
personSchema.parse(invalidPerson);
} catch (error:any) { //error 类型为 ZodError 类型
console.error("Invalid person:", error.errors); // 校验失败,抛出 ZodError
}
try {
const parsedColor = colorSchema.parse("red");
console.log("Valid color:", parsedColor);
} catch (error) {
console.error("Invalid color:", error);
}
try {
const parsedColor = colorSchema.parse("purple");
} catch (error:any) {
console.error("Invalid color:", error.errors);
}
三、Schema 的进阶用法:Refine 和 Transform
Zod 提供了 refine
和 transform
两个强大的方法,让你对 schema 进行更精细的控制。
-
refine
:自定义校验逻辑refine
允许你添加自定义的校验逻辑,对数据进行更复杂的验证。import { z } from "zod"; const passwordSchema = z.string().min(8).refine( (password) => /[A-Z]/.test(password), { message: "密码必须包含至少一个大写字母", } ); try { passwordSchema.parse("password"); // 校验失败 } catch (error:any) { console.error(error.errors); } try { passwordSchema.parse("Password123"); // 校验成功 } catch (error) { console.error(error); }
-
transform
:数据转换transform
允许你在校验通过后对数据进行转换。import { z } from "zod"; const dateStringSchema = z.string().transform((str) => new Date(str)); try { const date = dateStringSchema.parse("2023-10-27"); console.log(date); // 输出 Date 对象 } catch (error) { console.error(error); }
transform
也可以用于数据清洗:import { z } from "zod"; const nameSchema = z.string().transform((str) => str.trim()); const parsedName = nameSchema.parse(" Alice "); console.log(parsedName); // 输出 "Alice"
四、Zod 的运行时校验:parse
、safeParse
和 infer
Zod 提供了 parse
和 safeParse
两种方法来进行运行时校验。
-
parse
:校验失败时抛出错误parse
方法会校验数据是否符合 schema,如果校验失败,会抛出一个ZodError
异常。import { z } from "zod"; const numberSchema = z.number(); try { const parsedNumber = numberSchema.parse("123"); // 抛出 ZodError console.log(parsedNumber); } catch (error:any) { console.error(error.errors); }
-
safeParse
:校验失败时不抛出错误,返回结果对象safeParse
方法也校验数据是否符合 schema,但如果校验失败,不会抛出异常,而是返回一个包含success
属性和error
属性的对象。import { z } from "zod"; const numberSchema = z.number(); const result = numberSchema.safeParse("123"); if (result.success) { console.log(result.data); } else { console.error(result.error.errors); }
safeParse
更适合在需要处理校验失败情况的场景中使用,比如 API 接口的数据校验。 -
infer
:从 Schema 中推断 TypeScript 类型infer
是 Zod 提供的一个实用工具,它可以从 schema 中自动推断 TypeScript 类型。import { z } from "zod"; const personSchema = z.object({ name: z.string(), age: z.number(), }); type Person = z.infer<typeof personSchema>; // 从 personSchema 推断出 Person 类型 const person: Person = { name: "Bob", age: 40, };
使用
infer
可以避免手动编写类型定义,保持类型定义与 schema 的同步。
五、Zod 在实际项目中的应用
Zod 在实际项目中有很多应用场景,比如:
- API 接口的数据校验: 校验 API 请求的参数和响应的数据,确保数据的正确性和安全性。
- 表单数据校验: 校验用户提交的表单数据,防止无效数据进入系统。
- 配置文件校验: 校验配置文件的格式和内容,确保配置文件的正确性。
- 数据清洗和转换: 对外部数据进行清洗和转换,使其符合系统的数据格式。
一个 API 接口数据校验的例子:
import { z } from "zod";
// 定义 API 响应数据的 schema
const productSchema = z.object({
id: z.number(),
name: z.string(),
price: z.number(),
description: z.string().optional(),
});
type Product = z.infer<typeof productSchema>;
async function getProduct(id: number): Promise<Product> {
const response = await fetch(`/api/products/${id}`);
const data = await response.json();
// 校验 API 响应数据
try {
const parsedProduct = productSchema.parse(data);
return parsedProduct;
} catch (error:any) {
console.error("Invalid product data:", error.errors);
throw new Error("Invalid product data");
}
}
// 使用 getProduct 函数
getProduct(1)
.then((product) => {
console.log(product);
})
.catch((error) => {
console.error(error);
});
六、Zod 的高级特性:extend
、partial
和 pick
Zod 还提供了一些高级特性,让你更灵活地操作 schema。
-
extend
:扩展现有 schemaextend
允许你基于现有 schema 创建新的 schema,添加或覆盖字段。import { z } from "zod"; const baseSchema = z.object({ id: z.number(), name: z.string(), }); const extendedSchema = baseSchema.extend({ price: z.number(), description: z.string().optional(), }); type ExtendedType = z.infer<typeof extendedSchema>; const extendedObject:ExtendedType = { id: 1, name: "Product", price: 100, description: "Description" }
-
partial
:将 schema 的所有字段设置为可选partial
允许你将 schema 的所有字段设置为可选。import { z } from "zod"; const personSchema = z.object({ name: z.string(), age: z.number(), }); const partialPersonSchema = personSchema.partial(); type PartialPerson = z.infer<typeof partialPersonSchema>; const partialPerson:PartialPerson = { name: "Person" }
-
pick
:从 schema 中选择部分字段pick
允许你从 schema 中选择部分字段,创建一个新的 schema。import { z } from "zod"; const personSchema = z.object({ name: z.string(), age: z.number(), email: z.string().email(), }); const nameAndEmailSchema = personSchema.pick({ name: true, email: true }); type NameAndEmail = z.infer<typeof nameAndEmailSchema>; const nameAndEmail:NameAndEmail = { name: "Person", email: "[email protected]" }
七、Zod 的性能考量
Zod 在运行时进行类型校验,会带来一定的性能开销。对于性能敏感的应用,需要注意以下几点:
- 避免过度校验: 只在必要的时候进行校验,避免对已经校验过的数据重复校验。
- 优化 schema 定义: 尽量使用简单的 schema 类型,避免使用过于复杂的 schema 结构。
- 缓存 schema: 对于常用的 schema,可以将其缓存起来,避免重复创建。
不过,一般来说,Zod 的性能开销是可以接受的。相比于运行时错误带来的损失,Zod 提供的类型安全性和数据质量更有价值。
八、Zod 与其他校验库的比较
除了 Zod,还有一些其他的 JavaScript 校验库,比如 Joi、Yup 等。Zod 的优势在于:
- TypeScript 优先: Zod 本身就是用 TypeScript 写的,与 TypeScript 项目配合更默契。
- 类型推断: Zod 可以自动推断 TypeScript 类型,减少手动编写类型定义的工作。
- 简洁的 API: Zod 的 API 设计简洁易用,学习成本较低。
当然,其他校验库也有自己的特点和优势,选择哪个库取决于你的具体需求和偏好。
九、总结
Zod 是一个强大的 TypeScript 运行时类型校验库,它可以帮助你提高代码的健壮性、数据质量和开发效率。通过本文的介绍,相信你已经对 Zod 有了初步的了解。在实际项目中,可以根据你的需求灵活运用 Zod 提供的各种特性,打造更可靠的应用程序。
好了,今天的讲座就到这里。感谢各位观众老爷们的观看,祝大家 Bug 越来越少,代码越写越溜!下次再见!