Zod 是一个以 TypeScript 为首的形式声明和验证库 ,弥补了 TypeScript 无法在运转时进行校验的问题

Zod 既能够用在服务端也能够运转在客户端,以保障 Web Apps 的类型安全

接下来会用十个有趣的比方,带你快速入门 Zod,体会 Zod 的强大和便当 ~ 感谢 Matt Pocock 大神供给的 示例

本文库房地址 传送门

提示:本文 Star Wars API 有时会有超时状况,如遇超时则重试几遍哈

01 – 运用 Zod 进行运转时类型校验

问题

TypeScript 是一个十分有用的类型东西,用于查看代码中变量的类型

可是咱们不能总是确保代码中变量的类型,比方这些变量来自 API 接口或许表单输入

Zod 库使得咱们能够在 运转时 查看变量的类型,它对于咱们的大部分项目都是有用的

初探运转时查看

看看这个 toString 函数:

export const toString = (num: unknow) => {
    return String(num)
}

咱们将 num 的入参设置为 unknow

这意味着咱们能够在编码过程中给 toString 函数传递任何类型的参数,包括 object 类型或许 undefined :

toString('blah')
toString(undefined)
toString({name: 'Matt'})

到目前为止仍是没有报错的,但咱们想要在 运转时 防止这样的工作发生

假如咱们给 toString 传入一个字符串,咱们想要抛出一个过错,并提示预期传入一个数字可是接纳到一个字符串

it("当入参不是数字的时分,需求抛出一个过错", () => {
  expect(() => toString("123")).toThrowError(
    "Expected number, received string",
  );
});

假如咱们传入一个数字,toString 是能够正常运转的

it("当入参是数字的时分,需求回来一个字符串", () => {
    expect(toString(1)).toBeTypeOf("string");
});

解决计划

创立一个 numberParser

各种 Parser 是 Zod 最根底的功用之一

咱们经过 z.number() 来创立一个 numberParser

它创立了 z.ZodNumber 目标,这个目标供给了一些有用的办法

const numberParser = z.number();

假如数据不是数字类型的话,那么将这些数据传进 numberParser.parse() 后会报错

这就意味着,一切传进 numberParser.parse() 的变量都会被转成数字,然后咱们的测验才能够经过。

增加 numberParser , 更新 toString 办法


const numberParser = z.number();
export const toString = (num: unknown) => {
  const parsed = numberParser.parse(num);
  return String(parsed);
};

测验不同的类型

Zod 也答应其他的类型查验

比方,假如咱们要接纳的参数不是数字而是一个 boolean 值,那么咱们能够把 numberParser 修正成 z.boolean()

当然,假如咱们只修正了这个,那么咱们原有的测验用例就会报错哦

Zod 的这种技能为咱们供给了坚实的根底。 随着咱们的深化运用,你会发现 Zod 模仿了许多你在 TypeScript 中习惯的东西。

能够在 这儿 查看 Zod 完好的根底类型

02 – 运用 Object Schema 对不知道的 API 进行校验

问题

Zod 经常被用于校验不知道的 API 回来内容

在下面这个比方中,咱们从 Star Wars API 中获取一个人物的信息

export const fetchStarWarsPersonName = async (id: string) => {
  const data = await fetch("<https://swapi.dev/api/people/>" + id).then((res) =>
    res.json(),
  );
  const parsedData = PersonResult.parse(data);
  return parsedData.name;
};

留意到现在 PersonResult.parser() 处理的数据是从 fetch 请求来的

PersonResult 变量是由 z.unknown() 创立的,这告知咱们数据是被认为是 unknown 类型由于咱们不知道这些数据里边包括有什么

const PersonResult = z.unknown();

运转测验

假如咱们是用 console.log(data) 打印出 fetch 函数的回来值,咱们能够看到这个 API 回来的内容有许多,不仅仅有人物的 name ,还有其他的比方 eye_color,skin_color 等等一些咱们不感兴趣的内容

接下来咱们需求修正这个 PersonResult 的 unknown 类型

解决计划

运用 z.object 来修正 PersonResult

首要,咱们需求将 PersonResult 修正为 z.object

它答应咱们运用 key 和类型来界说这些 object

在这个比方中,咱们需求界说 name 成为字符串

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

留意到这儿有点像咱们在 TypeScript 中创立 interface

interface PersonResult {
    name: string;
}

查看咱们的工作

fetchStarWarsPersonName 中,咱们的 parsedData 现在现已被赋予了正确的类型,而且具有了一个 Zod 能辨认的结构

重新调用 API 咱们依然能够看到回来的数据里边有许多咱们不感兴趣的信息

现在假如咱们用 console.log 打印 parsedData,咱们能够看到 Zod 现已帮咱们过滤掉咱们不感兴趣的 Key 了,只给咱们 name 字段

更多

任何额外加入 PersonResult 的 key 都会被增加到 parsedData

能够显式的指明数据中每个 key 的类型是 Zod 中一个十分有用的功用

03 – 创立自界说类型数组

问题

在这个比方中,咱们依然运用 Star Wars API,可是这一次咱们要拿到 一切 人物的数据

一开端的部分跟咱们之前看到的十分相似,StarWarsPeopleResults 变量会被设置为 z.unknown()

const StarWarsPeopleResults = z.unknown();
export const fetchStarWarsPeople = async () => {
  const data = await fetch("https://swapi.dev/api/people/").then((res) =>
    res.json(),
  );
  const parsedData = StarWarsPeopleResults.parse(data);
  return parsedData.results;
};

跟之前相似,增加 console.log(data) 到 fetch 函数中,咱们能够看到数组中有许多数据即便咱们只对数组中的 name 字段感兴趣

假如这是一个 TypeScript 的 interface,它可能是需求写成这样

interface Results {
  results: {
    name: string;
  }[];
}

作业

经过运用 object schema 更新 StarWarsPeopleResults ,来表明一个 StarWarsPerson 目标的数组

能够参考这儿的文档来取得协助

解决计划

正确的解法便是创立一个目标来饮用其他的目标。在这个比方中,StarWarsPeopleResults 将是一个包括 results 特点的 z.object

关于 results,咱们运用 z.array 并供给 StarWarsPerson 作为参数。咱们也不用重复写 name: z.string() 这部分了

这个是之前的代码

const StarWarsPeopleResults = z.unknown()

修正之后

const StarWarsPeopleResults = z.object({
  results: z.array(StarWarsPerson),
});

假如咱们 console.log 这个 parsedData ,咱们能够取得期望的数据

像上面这样声明数组的 object 是 z.array() 最常用的的功用一直,特别是当这个 object 现已创立好了。

04 – 提取目标类型

问题

现在咱们运用 console 函数将 StarWarsPeopleResults 打印到操控台

const logStarWarsPeopleResults = (data: unknown) => {
  data.results.map((person) => {
    console.log(person.name);
  });
};

再一次,data 的类型是 unknown

为了修正,可能会测验运用下面这样的做法:

const logStarWarsPeopleResults = (data: typeof StarWarsPeopleResults)

可是这样仍是会有问题,由于这个类型代表的是 Zod 目标的类型而不是 StarWarsPeopleResults 类型

作业

更新 logStarWarsPeopleResults 函数去提取目标类型

解决计划

更新这个打印函数

运用 z.infer 而且传递 typeof StarWarsPeopleResults 来修正问题

const logStarWarsPeopleResults = (
  data: z.infer<typeof StarWarsPeopleResults>,
) => {
  ...

现在当咱们在 VSCode 中把鼠标 hover 到这个变量上,咱们能够看到它的类型是一个包括了 results 的 object

当咱们更新了 StarWarsPerson 这个 schema,函数的 data 也会同步更新

这是一个很棒的办法,它做到运用 Zod 在运转时进行类型查看,一同也能够在构建时获取数据的类型

一个替代计划

当然,咱们也能够把 StarWarsPeopleResultsType 保存为一个类型并将它从文件中导出

export type StarWarsPeopleResultsType = z.infer<typeof StarWarsPeopleResults>;

logStarWarsPeopleResults 函数则会被更新成这样

const logStarWarsPeopleResults = (data: StarWarsPeopleResultsType) => {
  data.results.map((person) => {
    console.log(person.name);
  });
};

这样其他文件也能够获取到 StarWarsPeopleResults 类型,假如需求的话

05 – 让 schema 变成可选的

问题

Zod 在前端项目中也相同是有用的

在这个比方中,咱们有一个函数叫做 validateFormInput

这儿 values 的类型是 unknown,这样做是安全的由于咱们不是特别了解这个 form 表单的字段。在这个比方中,咱们收集了 namephoneNumber 作为 Form 目标的 schema

const Form = z.object({
  name: z.string(),
  phoneNumber: z.string(),
});
export const validateFormInput = (values: unknown) => {
  const parsedData = Form.parse(values);
  return parsedData;
};

目前的状况来说,咱们的测验会报错假如 phoneNumber 字段没有被提交

作业

由于 phoneNumber 不总是必要的,需求想一个计划,不论 phoneNumber 是否有提交,咱们的测验用例都能够经过

解决计划

在这种状况下,解决计划十分直观!
phoneNumber schema 后边增加 .optional(),咱们的测验将会经过

const Form = z.object({ name: z.string(), phoneNumber: z.string().optional(), });

咱们说的是, name 字段是一个必填的字符串,phoneNumber 可能是一个字符串或许 undefined

咱们不需求再做更多什么额外的工作,让这个 schema 变成可选的便是一个十分不错的计划

06 – 在 Zod 中设置默认值

问题

咱们的下一个比方跟之前的很像:一个支持可选值的 form 表单输入校验器

这一次,Form 有一个 repoName 字段和一个可选数组字段 keywords

const Form = z.object({
  repoName: z.string(),
  keywords: z.array(z.string()).optional(),
});

为了使实际表单更简略,咱们希望对其进行设置,以便不用传入字符串数组。

作业

修正 Form 使得当 keywords 字段为空的时分,会有一个默认值(空数组)

解决计划

Zod 的 default schema 函数,答应当某个字段没有传参时供给一个默认值

在这个比方中,咱们将会运用 .default([]) 设置一个空数组

修正前

keywords: z.array(z.string()).optional()

修正后

keywords: z.array(z.string()).default([])

由于咱们增加了默认值,所以咱们不需求再运用 optional() ,optional 现已是被包括在其中了。

修正之后,咱们的测验能够经过了

输入不同于输出

在 Zod 中,咱们现已做到了输入与输出不同的地步。

也便是说,咱们能够做到基于输入生成类型也能够基于输出生成类型

比方,咱们创立 FormInputFormOutput 类型

type FormInput = z.infer<typeof Form>
type FormOutput = z.infer<typeof Form>

介绍 z.input

就像上面写的,输入不完全正确,由于当咱们在给 validateFormInput 传参数时,咱们没有必要一定要传递 keywords 字段

咱们能够运用 z.input 来替代 z.infer 来修正咱们的 FormInput

假如验证函数的输入和输出之间存在差异,则为咱们供给了另外一种生成的类型的办法。

type FormInput = z.input<typeof Form>

07 – 清晰答应的类型

问题

在这个比方中,咱们将再一次校验表单

这一次,Form 表单有一个 privacyLevel 字段,这个字段只答应 private 或许 public 这两个类型

const Form = z.object({
  repoName: z.string(),
  privacyLevel: z.string(),
});

假如是在 TypeScript 中,咱们会这么写

type PrivacyLevel = 'private' | 'public'

当然,咱们能够在这儿运用 boolean 类型,但假如将来咱们还需求往 PrivacyLevel 中增加新的类型,那就不太适宜了。在这儿,运用联合类型或许枚举类型是愈加安全的做法。

作业

第一个测验报错了,由于咱们的 validateFormInput 函数有除了 “private” 或 “public” 以外的其他值传入 PrivacyLevel 字段

it("假如传入一个非法的 privacyLevel 值,则需求报错", async () => {
  expect(() =>
    validateFormInput({
      repoName: "mattpocock",
      privacyLevel: "something-not-allowed",
    }),
  ).toThrowError();
});

你的使命是要找到一个 Zod 的 API 来答应咱们清晰入参的字符串类型,以此来让测验能够顺利经过。

解决计划

联合 (Unions) & 字面量 (Literals)

第一个解决计划,咱们将运用 Zod 的 联合函数,再传一个包括 “private” 和 “public” 字面量 的数组

const Form = z.object({
  repoName: z.string(),
  privacyLevel: z.union([z.literal("private"), z.literal("public")]),
});

字面量能够用来表明:数字,字符串,布尔类型;不能用来表明目标类型

咱们能运用 z.infer 查看咱们 Form 的类型

type FormType = z.infer<typeof Form>

在 VS Code 中假如你把鼠标移到 FormType 上,咱们能够看到 privacyLevel 有两个可选值:”private” 和 “public”

可认为是愈加简洁的计划:枚举

经过 z.enum 运用 Zod 枚举,也能够做到相同的工作,如下:

const Form = z.object({
  repoName: z.string(),
  privacyLevel: z.enum(["private", "public"]),
});

咱们能够经过语法糖的办法来解析字面量,而不是运用 z.literal

这个办法不会发生 TypeScript 中的枚举类型,比方

enum PrivacyLevcel {
    private,
    public
}

一个新的联合类型会被创立

相同,咱们经过把鼠标移到类型上面,咱们能够看到一个新的包括 “private” 和 “public” 的联合类型

08 – 杂乱的 schema 校验

问题

到目前为止,咱们的表单校验器函数现已能够查看各种值了

表单具有 name,email 字段还有可选的 phoneNumber 和 website 字段

可是,咱们现在想对一些值做强束缚

需求约束用户不能输入不合法的 URL 以及电话号码

作业

你的使命是寻觅 Zod 的 API 来为表单类型做校验

电话号码需求是适宜的字符,邮箱地址和 URL 也需求正确的格式

解决计划

Zod 文档的字符串章节包括了一些校验的比方,这些能够协助咱们顺利经过测验

现在咱们的 Form 表单 schema 会是写成这样

const Form = z.object({
  name: z.string().min(1),
  phoneNumber: z.string().min(5).max(20).optional(),
  email: z.string().email(),
  website: z.string().url().optional(),
});

name 字段加上了 min(1),由于咱们不能给它传空字符串

phoneNumber 约束了字符串长度是 5 至 20,一同它是可选的

Zod 有内建的邮箱和 url 校验器,咱们能够不需求自己手动编写这些规则

能够留意到,咱们不能这样写 .optional().min(), 由于optional 类型没有 min 特点。这意味着咱们需求将 .optional() 写在每个校验器后边

还有许多其他的校验器规则,咱们能够在 Zod 文档中找到

09 – 经过组合 schema 来削减重复

问题

现在,咱们来做一些不一样的工作

在这个比方中,咱们需求寻觅计划来重构项目,以削减重复代码

这儿咱们有这些 schema,包括:User, PostComment

const User = z.object({
  id: z.string().uuid(),
  name: z.string(),
});
const Post = z.object({
  id: z.string().uuid(),
  title: z.string(),
  body: z.string(),
});
const Comment = z.object({
  id: z.string().uuid(),
  text: z.string(),
});

咱们看到, id 在每个 schema 都呈现了

Zod 供给了许多计划能够将 object 目标安排到不同的类型中,使得咱们能够让咱们的代码愈加契合 DRY 准则

作业

你的应战是,需求运用 Zod 进行代码重构,来削减 id 的重复编写

关于测验用例语法

你不用忧虑这个测验用例的 TypeScript 语法,这儿有个快速的解释:

Expect<
  Equal<z.infer<typeof Comment>, { id: string; text: string }>
>

在上面的代码中,Equal 是确认 z.infer<typeof Comment>{id: string; text: string} 是相同的类型

假如你删除掉 Commentid 字段,那么在 VS Code 中能够看到 Expect 会有一个报错,由于这个比较不成立了

解决计划

咱们有许多办法能够重构这段代码

作为参考,这是咱们开端的内容:

const User = z.object({
  id: z.string().uuid(),
  name: z.string(),
});
const Post = z.object({
  id: z.string().uuid(),
  title: z.string(),
  body: z.string(),
});
const Comment = z.object({
  id: z.string().uuid(),
  text: z.string(),
});

简略的计划

最简略的计划是抽取 id 字段保存成一个单独的类型,然后每一个 z.object 都能够引用它

const Id = z.string().uuid();
const User = z.object({
  id: Id,
  name: z.string(),
});
const Post = z.object({
  id: Id,
  title: z.string(),
  body: z.string(),
});
const Comment = z.object({
  id: Id,
  text: z.string(),
});

这个计划挺不错,可是 id: ID 这段仍然是一直在重复。一切的测验都能够经过,所以也还行

运用扩展(Extend)办法

另一个计划是创立一个叫做 ObjectWithId 的根底目标,这个目标包括 id 字段

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

咱们能够运用扩展办法去创立一个新的 schema 来增加根底目标

const ObjectWithId = z.object({
  id: z.string().uuid(),
});
const User = ObjectWithId.extend({
  name: z.string(),
});
const Post = ObjectWithId.extend({
  title: z.string(),
  body: z.string(),
});
const Comment = ObjectWithId.extend({
  text: z.string(),
});

请留意,.extend() 会覆盖字段

运用兼并(Merge)办法

跟上面的计划相似,咱们能够运用兼并办法来扩展根底目标 ObjectWithId :

const User = ObjectWithId.merge(
  z.object({
    name: z.string(),
  }),
);

运用 .merge() 会比 .extend() 愈加冗长。咱们有必要传一个包括 z.string()z.object() 目标

兼并通常是用于联合两个不同的类型,而不是仅仅用来扩展单个类型

这些是在 Zod 中将目标组合在一同的几种不同办法,以削减代码重复量,使代码愈加契合 DRY,并使项目更易于保护!

10 – 经过 schema 转化数据

问题

Zod 的另一个十分有用的功用是操控从 API 接口响应的数据

现在咱们翻回去看看 Star Wars 的比方

想起咱们创立了 StarWarsPeopleResults , 其中 results 字段是一个包括 StarWarsPerson schema 的数组

当咱们从 API 获取 StarWarsPersonname,咱们获取的是他们的全称

现在咱们要做的是为 StarWarsPerson 增加转化

作业

你的使命是为这个根底的 StarWarsPerson 目标增加一个转化,将 name 字段依照空格切割成数组,并将数组保存到 nameAsArray 字段中

测验用例大概是这样的:

it("需求解析 name 和 nameAsArray 字段", async () => {
  expect((await fetchStarWarsPeople())[0]).toEqual({
    name: "Luke Skywalker",
    nameAsArray: ["Luke", "Skywalker"],
  });
});

解决计划

提示一下,这是 StarWarsPerson 在转化前的姿态:

const StarWarsPerson = z.object({
  name: z.string()
});

增加一个转化 (Transformation)

当咱们在 .object() 中的 name 字段时,咱们能够获取 person 参数,然后转化它并增加到一个新的特点中

const StarWarsPerson = z
  .object({
    name: z.string(),
  })
  .transform((person) => ({
    ...person,
    nameAsArray: person.name.split(" "),
  }));

.transform() 内部,person 是上面包括 name 的目标。

这也是咱们增加满意测验的 nameAsArray 特点的当地。

一切这些都发生在 StarWarsPerson 这个效果域中,而不是在 fetch 函数内部或其他当地。

另一个比方

Zod 的转化 API 适用于它的任何原始类型。

比方,咱们能够转化 namez.object 的内部

const StarWarsPerson = z
  .object({
    name: z.string().transform((name) => `Awesome ${name}`)
  }),
  ...

现在咱们具有一个 name 字段包括 Awesome Luke Skywalker 和一个 nameAsArray 字段包括 ['Awesome', 'Luke', 'Skywalker']

转化过程在最底层起效果,能够组合,而且十分有用

总结

以上便是教程的一切内容,后续还会一直补充更多的有用比方,主张收藏 ~ 也欢迎各位小伙伴看完之后能跟我一同评论有关于 Zod 的相关问题,提出宝贵意见 ~

引用文献

  • www.totaltypescript.com/tutorials/z…
  • zod.dev/