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 表单的字段。在这个比方中,咱们收集了 name
和 phoneNumber
作为 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 中,咱们现已做到了输入与输出不同的地步。
也便是说,咱们能够做到基于输入生成类型也能够基于输出生成类型
比方,咱们创立 FormInput
和 FormOutput
类型
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
, Post
和 Comment
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}
是相同的类型
假如你删除掉 Comment
的 id
字段,那么在 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 获取 StarWarsPerson
的 name
,咱们获取的是他们的全称
现在咱们要做的是为 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 适用于它的任何原始类型。
比方,咱们能够转化 name
在 z.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/