乐趣区

关于typescript:超详细Zod-入门教程

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 的相干问题,提出宝贵意见 ~

援用文献

  • https://www.totaltypescript.c…
  • https://zod.dev/
退出移动版