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 的相干问题,提出宝贵意见 ~
援用文献
- https://www.totaltypescript.c…
- https://zod.dev/