JavaScript内核与高级编程之:`TypeScript` 的 `Zod` 库:其在运行时类型校验中的原理和应用。

各位观众老爷们,晚上好!我是今晚的讲师,人称 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 提供了 refinetransform 两个强大的方法,让你对 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 的运行时校验:parsesafeParseinfer

Zod 提供了 parsesafeParse 两种方法来进行运行时校验。

  • 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 的高级特性:extendpartialpick

Zod 还提供了一些高级特性,让你更灵活地操作 schema。

  • extend:扩展现有 schema

    extend 允许你基于现有 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 越来越少,代码越写越溜!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注