利用静态类型推断进行 TypeScript 优先模式验证
https://zod.dev
- 什么是 Zod
- 生态体系
- 安装
- 基本用法
- 原始类型
- 原始类型的强制转换
- 字面量
- 字符串
- Numbers
- Objects
- Records
- Maps
- Sets
- Arrays
- Unions
- Discriminated unions
- Optionals
- Nullables
- Enums
- Tuples
- Recursive types
- Promises
- Instanceof
- Function schemas
- 基础类方法 (ZodType)
- 类型推断
- Errors
- 比较
- Changelog
Zod 是一个 TypeScript 优先的模式声明和验证库。我使用术语 "模式" 来广义地指任何数据类型,从简单的 字符串
到复杂的嵌套对象。
Zod 围绕尽可能友好的开发体验而设计。其目的是消除重复的类型声明。使用 Zod,你只需声明 一次 验证器,Zod 就会自动推断出静态 TypeScript 类型。将简单类型组合成复杂的数据结构非常容易。
其他一些重要方面:
- 零依赖
- 适用于 Node.js 和所有现代浏览器
- 小巧: 压缩后仅 8kb
- 不可变: 方法 (如
.optional()
) 返回一个新的实例 - 简洁的、可链式调用的接口
- 函数式方法: 解析,不验证
- 也可用于纯 JavaScript! 你不需要使用 TypeScript。
我们感谢并鼓励任何级别的赞助。Zod 是由一个单独的开发者维护的 (hi!)。对于个人开发者,可以考虑一杯咖啡级别。如果你使用 Zod 建立了一个付费产品,可以考虑领奖台级别。
Astro astro.build
Astro is a new kind of static |
Glow Wallet glow.app Your new favorite
|
Deletype deletype.com |
Snaplet snaplet.dev |
Marcato Partners marcatopartners.com |
Trip |
Seasoned Software seasoned.cc |
Interval interval.com |
Brandon Bayer @flybayer, creator of Blitz.js |
Jiří Brabec @brabeji |
Alex Johansson @alexdotjs |
Adaptable adaptable.io |
Avana Wallet avanawallet.com Solana non-custodial wallet |
要在这里看到你的名字 + Twitter + 網站 , 请在Freelancer 或 Consultancy赞助 Zod .
有越来越多的工具是建立在 Zod 之上或原生支持 Zod 的! 如果你在 Zod 的基础上建立了一个工具或库,请在Twitter 或者 Discussion上告诉我。我会把它添加到下面,并在推特上发布。
tRPC
: 在没有 GraphQL 的情况下建立端到端的类型安全 APIreact-hook-form
: 使用 React Hook Form 和 Zod 解析器轻松构建类型安全的表单。ts-to-zod
: 将 TypeScript 定义转换成 Zod 模式。zod-mocking
: 从你的 Zod 模式中生成模拟数据。zod-fast-check
: 从 Zod 模式中生成fast-check
的任意数据。zod-endpoints
: 约定优先的严格类型的端点与 Zod。兼容 OpenAPI。express-zod-api
: 用 I/O 模式验证和自定义中间件构建基于 Express 的 API 服务zod-i18n-map
: 有助于翻译 zod 错误信息。mobx-zod-form
: 以数据为中心的表格构建工具,基于 MobX 和 Zod。zodock
: 基於 Zod 模式生成模擬數據。
- TypeScript 4.5+!
- 你必须在你的
tsconfig.json
中启用strict
模式。这是所有 TypeScript 项目的最佳实践。
// tsconfig.json
{
// ...
"compilerOptions": {
// ...
"strict": true
}
}
npm install zod
deno add npm:zod # deno
yarn add zod # yarn
bun add zod # bun
pnpm add zod # pnpm
README 的剩余部分假定你是直接通过 npm 安装的
zod
包。
创建一个简单的字符串模式
import { z } from "zod";
// 创建一个字符串的模式
const mySchema = z.string();
// 解析
mySchema.parse("tuna"); // => "tuna"
mySchema.parse(12); // => throws ZodError
// "安全"解析(如果验证失败不抛出错误)
mySchema.safeParse("tuna"); // => { success: true; data: "tuna" }
mySchema.safeParse(12); // => { success: false; error: ZodError }
创建一个对象模式
import { z } from "zod";
const User = z.object({
username: z.string(),
});
User.parse({ username: "Ludwig" });
// 提取出推断的类型
type User = z.infer<typeof User>;
// { username: string }
import { z } from "zod";
// 原始值类型
z.string();
z.number();
z.bigint();
z.boolean();
z.date();
z.symbol();
// 空类型
z.undefined();
z.null();
z.void(); // 接受 undefined
// 任意类型
// 允许任意类型的值
z.any();
z.unknown();
// never 类型
// 不允许值类型存在
z.never();
Zod 现在提供了一种更方便的方法来强制转换原始类型
const schema = z.coerce.string();
schema.parse("tuna"); // => "tuna"
schema.parse(12); // => "12"
schema.parse(true); // => "true"
在解析步骤中,输入将通过 String()
函数传递,该函数是 JavaScript 的内置函数,用于将数据强制转换为字符串。请注意,返回的模式是一个 ZodString
实例,因此可以使用所有字符串方法
z.coerce.string().email().min(5);
所有的原始类型都支持强制转换
z.coerce.string(); // String(input)
z.coerce.number(); // Number(input)
z.coerce.boolean(); // Boolean(input)
z.coerce.bigint(); // BigInt(input)
z.coerce.date(); // new Date(input)
布尔类型的强制转换
Zod 的布尔强制非常简单!它将值传入 Boolean(value)
函数,仅此而已。任何真值都将解析为 true
,任何假值都将解析为 false
z.coerce.boolean().parse("tuna"); // => true
z.coerce.boolean().parse("true"); // => true
z.coerce.boolean().parse("false"); // => true
z.coerce.boolean().parse(1); // => true
z.coerce.boolean().parse([]); // => true
z.coerce.boolean().parse(0); // => false
z.coerce.boolean().parse(undefined); // => false
z.coerce.boolean().parse(null); // => false
const tuna = z.literal("tuna");
const twelve = z.literal(12);
const twobig = z.literal(2n); // bigint literal
const tru = z.literal(true);
const terrificSymbol = Symbol("terrific");
const terrific = z.literal(terrificSymbol);
// 检索字面量的值
tuna.value; // "tuna"
目前在 Zod 中不支持 Date 字面量。如果你有这个功能的用例,请提交一个 Issue。
Zod 包括一些针对字符串的验证。
// 验证
z.string().max(5);
z.string().min(5);
z.string().length(5);
z.string().email();
z.string().url();
z.string().emoji();
z.string().uuid();
z.string().cuid();
z.string().cuid2();
z.string().ulid();
z.string().duration();
z.string().regex(regex);
z.string().includes(string);
z.string().startsWith(string);
z.string().endsWith(string);
z.string().datetime(); // ISO 8601;默认值为无 UTC 偏移,选项见下文
z.string().ip(); // 默认为 IPv4 和 IPv6,选项见下文
// 转变
z.string().trim(); // 减除空白
z.string().toLowerCase(); // 小写化
z.string().toUpperCase(); // 大写化
请查看 validator.js,了解可与 Refinements 结合使用的大量其他有用字符串验证函数。
创建字符串模式时,你可以自定义一些常见的错误信息
const name = z.string({
required_error: "Name is required",
invalid_type_error: "Name must be a string",
});
使用验证方法时,你可以传递一个附加参数,以提供自定义错误信息
z.string().min(5, { message: "Must be 5 or more characters long" });
z.string().max(5, { message: "Must be 5 or fewer characters long" });
z.string().length(5, { message: "Must be exactly 5 characters long" });
z.string().email({ message: "Invalid email address" });
z.string().url({ message: "Invalid url" });
z.string().emoji({ message: "Contains non-emoji characters" });
z.string().uuid({ message: "Invalid UUID" });
z.string().includes("tuna", { message: "Must include tuna" });
z.string().startsWith("https://", { message: "Must provide secure URL" });
z.string().endsWith(".com", { message: "Only .com domains allowed" });
z.string().datetime({ message: "Invalid datetime string! Must be UTC." });
z.string().ip({ message: "Invalid IP address" });
z.string().datetime()
方法执行 ISO 8601;默认为无时区偏移和任意的小数点后几秒精度
const datetime = z.string().datetime();
datetime.parse("2020-01-01T00:00:00Z"); // pass
datetime.parse("2020-01-01T00:00:00.123Z"); // pass
datetime.parse("2020-01-01T00:00:00.123456Z"); // pass (任意精度)
datetime.parse("2020-01-01T00:00:00+02:00"); // fail (不允许偏移)
将 offset
选项设置为 true
,可允许时区偏移
const datetime = z.string().datetime({ offset: true });
datetime.parse("2020-01-01T00:00:00+02:00"); // pass
datetime.parse("2020-01-01T00:00:00.123+02:00"); // pass (毫秒数可选)
datetime.parse("2020-01-01T00:00:00.123+0200"); // pass (毫秒数可选)
datetime.parse("2020-01-01T00:00:00.123+02"); // pass (只偏移小时)
datetime.parse("2020-01-01T00:00:00Z"); // pass (仍支持 Z)
你还可以限制允许的 "精度"。默认情况下,支持任意亚秒精度(但可选)
const datetime = z.string().datetime({ precision: 3 });
datetime.parse("2020-01-01T00:00:00.123Z"); // pass
datetime.parse("2020-01-01T00:00:00Z"); // fail
datetime.parse("2020-01-01T00:00:00.123456Z"); // fail
默认情况下,z.string().ip()
方法会验证 IPv4 和 IPv6
const ip = z.string().ip();
ip.parse("192.168.1.1"); // pass
ip.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003"); // pass
ip.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:192.168.1.1"); // pass
ip.parse("256.1.1.1"); // fail
ip.parse("84d5:51a0:9114:gggg:4cfa:f2d7:1f12:7003"); // fail
你还可以设置 IP 版本
const ipv4 = z.string().ip({ version: "v4" });
ipv4.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003"); // fail
const ipv6 = z.string().ip({ version: "v6" });
ipv6.parse("192.168.1.1"); // fail
在创建数字模式时,你可以自定义某些错误信息
const age = z.number({
required_error: "Age is required",
invalid_type_error: "Age must be a number",
});
Zod 包括一些特定的数字验证。
z.number().gt(5);
z.number().gte(5); // alias .min(5)
z.number().lt(5);
z.number().lte(5); // alias .max(5)
z.number().int(); // value must be an integer
z.number().positive(); // > 0
z.number().nonnegative(); // >= 0
z.number().negative(); // < 0
z.number().nonpositive(); // <= 0
z.number().multipleOf(5); // Evenly divisible by 5. Alias .step(5)
z.number().finite(); // value must be finite, not Infinity or -Infinity
z.number().safe(); // value must be between Number.MIN_SAFE_INTEGER and Number.MAX_SAFE_INTEGER
你可以选择传入第二个参数来提供一个自定义的错误信息。
z.number().max(5, { message: "this👏is👏too👏big" });
z.date().safeParse(new Date()); // success: true
z.date({
required_error: "Please select a date and time",
invalid_type_error: "That's not a date!",
});
z.date().min(new Date("1900-01-01"), { message: "Too old" });
z.date().max(new Date(), { message: "Too young!" });
// 所有属性都是默认需要的
const Dog = z.object({
name: z.string(),
age: z.number(),
});
// 像这样提取推断出的类型
type Dog = z.infer<typeof Dog>;
// 相当于:
type Dog = {
name: string;
age: number;
};
使用.shape
来访问特定键的模式。
Dog.shape.name; // => string schema
Dog.shape.age; // => number schema
你可以用.extend
方法在对象模式中添加额外的字段。
const DogWithBreed = Dog.extend({
breed: z.string(),
});
你可以使用.extend
来覆盖字段! 要小心使用这种方式!
相当于 A.extend(B.shape)
.
const BaseTeacher = z.object({ students: z.array(z.string()) });
const HasID = z.object({ id: z.string() });
const Teacher = BaseTeacher.merge(HasID);
type Teacher = z.infer<typeof Teacher>; // => { students: string[], id: string }
如果两个模式共享 keys,那么 B 的属性将覆盖 A 的属性。返回的模式也继承了 "unknownKeys 密钥 "策略(strip/strict/passthrough+)和 B 的全面模式。
受 TypeScript 内置的Pick
和Omit
工具类型的启发,所有 Zod 对象模式都有.pick
和 .omit
方法,可以返回一个修改后的版本。考虑一下这个 Recipe 模式。
const Recipe = z.object({
id: z.string(),
name: z.string(),
ingredients: z.array(z.string()),
});
要想只保留某些 Key,使用 .pick
.
const JustTheName = Recipe.pick({ name: true });
type JustTheName = z.infer<typeof JustTheName>;
// => { name: string }
要删除某些 Key,请使用 .omit
.
const NoIDRecipe = Recipe.omit({ id: true });
type NoIDRecipe = z.infer<typeof NoIDRecipe>;
// => { name: string, ingredients: string[] }
受 TypeScript 内置的实用类型Partial的启发, .partial
方法使所有属性都是可选的。
从这个对象开始:
const user = z.object({
username: z.string(),
});
// { username: string }
我们可以创建一个 Partial 版本:
const partialUser = user.partial();
// { username?: string | undefined }
T.partial
只是一个浅层的使用 — 它只适用于一个层次的深度。还有一个 "深层" 版本:
const user = z.object({
username: z.string(),
location: z.object({
latitude: z.number(),
longitude: z.number(),
}),
});
const deepPartialUser = user.deepPartial();
/*
{
username?: string | undefined,
location?: {
latitude?: number | undefined;
longitude?: number | undefined;
} | undefined
}
*/
重要的限制:
deep partials
只在对象模式的直接层次中按预期工作。嵌套的对象模式不能是可选的,不能是空的,不能包含细化,不能包含转换,等等...
默认情况下,Zod 对象的模式在解析过程中会剥离出未被识别的 keys
const person = z.object({
name: z.string(),
});
person.parse({
name: "bob dylan",
extraKey: 61,
});
// => { name: "bob dylan" }
// extraKey已经被剥离
相反,如果你想通过未知的 keys,使用.passthrough()
。
person.passthrough().parse({
name: "bob dylan",
extraKey: 61,
});
// => { name: "bob dylan", extraKey: 61 }
你可以用.strict()
来 禁止 未知键。如果输入中存在任何未知的 keys,Zod 将抛出一个错误。
const person = z
.object({
name: z.string(),
})
.strict();
person.parse({
name: "bob dylan",
extraKey: 61,
});
// => throws ZodError
你可以使用.strip
方法将一个对象模式重置为默认行为(剥离未识别的 keys)。
你可以将一个 "catchall "模式传递给一个对象模式。所有未知的 keys 都将根据它进行验证。
const person = z
.object({
name: z.string(),
})
.catchall(z.number());
person.parse({
name: "bob dylan",
validExtraKey: 61, // 运行良好
});
person.parse({
name: "bob dylan",
validExtraKey: false, // 未能成功
});
// => throws ZodError
使用.catchall()
可以避免.passthrough()
,.strip()
,或.strict()
。现在所有的键都被视为 "已知(known)"。
const stringArray = z.array(z.string());
// 相当于
const stringArray = z.string().array();
要小心使用.array()
方法。它返回一个新的ZodArray
实例。这意味着你调用方法的 顺序 很重要。比如说:
z.string().optional().array(); // (string | undefined)[]
z.string().array().optional(); // string[] | undefined
如果你想确保一个数组至少包含一个元素,使用 .nonempty()
.
const nonEmptyStrings = z.string().array().nonempty();
// 现在推断的类型是
// [string, ...string[]]
nonEmptyStrings.parse([]); // throws: "Array cannot be empty"
nonEmptyStrings.parse(["Ariana Grande"]); // passes
z.string().array().min(5); // 必须包含5个或更多元素
z.string().array().max(5); // 必须包含5个或更少元素
z.string().array().length(5); // 必须正好包含5个元素
与.nonempty()
不同,这些方法不会改变推断的类型。
Zod 包括一个内置的z.union
方法,用于合成 "OR" 类型。
const stringOrNumber = z.union([z.string(), z.number()]);
stringOrNumber.parse("foo"); // 通过
stringOrNumber.parse(14); // 通过
Zod 将按照每个 "选项" 的顺序测试输入,并返回第一个成功验证的值。
为了方便,你也可以使用.or
方法:
const stringOrNumber = z.string().or(z.number());
判别联合模式是指联合类型有一个特定键,根据该键值命中对应的对象模式。
type MyUnion =
| { status: "success"; data: string }
| { status: "failed"; error: Error };
这种特殊的联合类型可以用 z.discriminatedUnion
方法来表示。Zod 可以检查判别键(上例中的 status
),以确定应使用哪种模式来解析输入。这不仅提高了解析效率,还让 Zod 可以更友好地报告错误。
如果使用基础的联合模式,输入会根据所提供的每个 "选项 "进行测试,如果无效,所有 "选项 "的问题都会显示在 zod 错误中。对于判别联合模式,只会对特定键值对应的 "选项" 进行测试,并只显示与该 "选项 "相关的问题。
const myUnion = z.discriminatedUnion("status", [
z.object({ status: z.literal("success"), data: z.string() }),
z.object({ status: z.literal("failed"), error: z.instanceof(Error) }),
]);
myUnion.parse({ status: "success", data: "yippie ki yay" });
可以使用 .options
属性获取选项列表。
myUnion.options; // [ZodObject<...>, ZodObject<...>]
要合并两个或更多判别联合模式,请展开所有模式中的 .options
。
const A = z.discriminatedUnion("status", [
/* options */
]);
const B = z.discriminatedUnion("status", [
/* options */
]);
const AB = z.discriminatedUnion("status", [...A.options, ...B.options]);
你可以用z.optional()
使任何模式成为可选:
const schema = z.optional(z.string());
schema.parse(undefined); // => returns undefined
type A = z.infer<typeof schema>; // string | undefined
你可以用.optional()
方法使一个现有的模式成为可选的:
const user = z.object({
username: z.string().optional(),
});
type C = z.infer<typeof user>; // { username?: string | undefined };
const stringSchema = z.string();
const optionalString = stringSchema.optional();
optionalString.unwrap() === stringSchema; // true
类似地,你可以这样创建 nullable 类型:
const nullableString = z.nullable(z.string());
nullableString.parse("asdf"); // => "asdf"
nullableString.parse(null); // => null
你可以用nullable
方法使一个现有的模式变成 nullable:
const E = z.string().nullable(); // equivalent to D
type E = z.infer<typeof E>; // string | null
const stringSchema = z.string();
const nullableString = stringSchema.nullable();
nullableString.unwrap() === stringSchema; // true
Record 模式用于验证诸如{ [k: string]: number }
这样的类型。
如果你想根据某种模式验证一个对象的 value ,但不关心 keys,使用`Record'。
const NumberCache = z.record(z.number());
type NumberCache = z.infer<typeof NumberCache>;
// => { [k: string]: number }
这对于按 ID 存储或缓存项目特别有用。
const userSchema = z.object({ name: z.string() });
const userStoreSchema = z.record(userSchema);
type UserStore = z.infer<typeof userStoreSchema>;
// => type UserStore = { [ x: string ]: { name: string } }
const userStore: UserStore = {};
userStore["77d2586b-9e8e-4ecf-8b21-ea7e0530eadd"] = {
name: "Carlotta",
}; // passes
userStore["77d2586b-9e8e-4ecf-8b21-ea7e0530eadd"] = {
whatever: "Ice cream sundae",
}; // TypeError
你可能期望z.record()
接受两个参数,一个是 key,一个是 value。毕竟,TypeScript 的内置 Record 类型是这样的:Record<KeyType, ValueType>
。否则,你如何在 Zod 中表示 TypeScript 类型Record<number, any>
?
事实证明,TypeScript 围绕[k: number]
的行为有点不直观:
const testMap: { [k: number]: string } = {
1: "one",
};
for (const key in testMap) {
console.log(`${key}: ${typeof key}`);
}
// prints: `1: string`
正如你所看到的,JavaScript 会自动将所有对象 key 转换为字符串。
由于 Zod 试图弥合静态类型和运行时类型之间的差距,提供一种创建带有数字键的记录模式的方法是没有意义的,因为在 JavaScript runtime 中没有数字键这回事。
const stringNumberMap = z.map(z.string(), z.number());
type StringNumberMap = z.infer<typeof stringNumberMap>;
// type StringNumber = Map<string, number>
const numberSet = z.set(z.number());
type numberSet = z.infer<typeof numberSet>;
// Set<number>
在 Zod 中,有两种方法来定义枚举。
const FishEnum = z.enum(["Salmon", "Tuna", "Trout"]);
type FishEnum = z.infer<typeof FishEnum>;
// 'Salmon' | 'Tuna' | 'Trout'
你必须将数值数组直接传入z.enum()
。这样做是不行的:
const fish = ["Salmon", "Tuna", "Trout"];
const FishEnum = z.enum(fish);
在这种情况下,Zod 无法推断出各个枚举元素;相反,推断出的类型将是 string
而不是'Salmon'|'Tuna'|'Trout'
。
另一种可行的方式是使用as const
,这样 Zod 就可以推断出正确的类型。
const VALUES = ["Salmon", "Tuna", "Trout"] as const;
const FishEnum = z.enum(VALUES);
自动补全
为了获得 Zod 枚举的自动完成,请使用你的模式的.enum
属性:
FishEnum.enum.Salmon; // => 自动补全
FishEnum.enum;
/*
=> {
Salmon: "Salmon",
Tuna: "Tuna",
Trout: "Trout",
}
*/
你也可以用.options
属性检索选项列表,作为一个元组:
FishEnum.options; // ["Salmon", "Tuna", "Trout"]);
Zod 枚举是定义和验证枚举的推荐方法。但是如果你需要对第三方库的枚举进行验证(或者你不想重写你现有的枚举),你可以使用z.nativeEnum()
。
数字枚举
enum Fruits {
Apple,
Banana,
}
const FruitEnum = z.nativeEnum(Fruits);
type FruitEnum = z.infer<typeof FruitEnum>; // Fruits
FruitEnum.parse(Fruits.Apple); // 通过
FruitEnum.parse(Fruits.Banana); // 通过
FruitEnum.parse(0); // 通过
FruitEnum.parse(1); // 通过
FruitEnum.parse(3); // 未通过
String enums
enum Fruits {
Apple = "apple",
Banana = "banana",
Cantaloupe, // 你可以混合使用数字和字符串的枚举
}
const FruitEnum = z.nativeEnum(Fruits);
type FruitEnum = z.infer<typeof FruitEnum>; // Fruits
FruitEnum.parse(Fruits.Apple); // 通过
FruitEnum.parse(Fruits.Cantaloupe); // 通过
FruitEnum.parse("apple"); // 通过
FruitEnum.parse("banana"); // 通过
FruitEnum.parse(0); // 通过
FruitEnum.parse("Cantaloupe"); // 未通过
常量枚举
.nativeEnum()
函数也适用于as const
对象。 as const
需要 TypeScript 3.4+!
const Fruits = {
Apple: "apple",
Banana: "banana",
Cantaloupe: 3,
} as const;
const FruitEnum = z.nativeEnum(Fruits);
type FruitEnum = z.infer<typeof FruitEnum>; // "apple" | "banana" | 3
FruitEnum.parse("apple"); // passes
FruitEnum.parse("banana"); // passes
FruitEnum.parse(3); // passes
FruitEnum.parse("Cantaloupe"); // fails
交叉类型对于创建 "logical AND"类型很有用。这对于两个对象类型的相交很有用。
const Person = z.object({
name: z.string(),
});
const Employee = z.object({
role: z.string(),
});
const EmployedPerson = z.intersection(Person, Employee);
// equivalent to:
const EmployedPerson = Person.and(Employee);
虽然在很多情况下,建议使用A.merge(B)
来合并两个对象。.merge
方法返回一个新的ZodObject
实例,而A.and(B)
返回一个不太有用的ZodIntersection
实例,它缺乏像pick
和omit
这样的常用对象方法。
const a = z.union([z.number(), z.string()]);
const b = z.union([z.number(), z.boolean()]);
const c = z.intersection(a, b);
type c = z.infer<typeof c>; // => number
与数组不同,tuples 有固定数量的元素,每个元素可以有不同的类型。
const athleteSchema = z.tuple([
z.string(), // name
z.number(), // jersey number
z.object({
pointsScored: z.number(),
}), // statistics
]);
type Athlete = z.infer<typeof athleteSchema>;
// type Athlete = [string, number, { pointsScored: number }]
你可以在 Zod 中定义一个递归模式,但由于 TypeScript 的限制,它们的类型不能被静态推断。相反,你需要手动定义类型,并将其作为 "类型提示" 提供给 Zod。
interface Category {
name: string;
subcategories: Category[];
}
// cast to z.ZodSchema<Category>
const Category: z.ZodSchema<Category> = z.lazy(() =>
z.object({
name: z.string(),
subcategories: z.array(Category),
})
);
Category.parse({
name: "People",
subcategories: [
{
name: "Politicians",
subcategories: [{ name: "Presidents", subcategories: [] }],
},
],
}); // 通过
不幸的是,这段代码有点重复,因为你声明了两次类型:一次在接口中,另一次在 Zod 定义中。
如果你想验证任何 JSON 值,你可以使用下面的片段。
const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
type Literal = z.infer<typeof literalSchema>;
type Json = Literal | { [key: string]: Json } | Json[];
const jsonSchema: z.ZodSchema<Json> = z.lazy(() =>
z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)])
);
jsonSchema.parse(data);
感谢ggoodman的建议。
尽管支持递归模式,但将一个循环数据传入 Zod 会导致无限循环。
const numberPromise = z.promise(z.number());
"Parsing"的工作方式与 promise 模式有点不同。验证分两部分进行:
- Zod 同步检查输入是否是 Promise 的实例(即一个具有
.then
和.catch
方法的对象)。 - Zod 使用
.then
在现有的 Promise 上附加一个额外的验证步骤。你必须在返回的 Promise 上使用.catch
来处理验证失败的问题。
numberPromise.parse("tuna");
// ZodError: Non-Promise type: string
numberPromise.parse(Promise.resolve("tuna"));
// => Promise<number>
const test = async () => {
await numberPromise.parse(Promise.resolve("tuna"));
// ZodError: Non-number type: string
await numberPromise.parse(Promise.resolve(3.14));
// => 3.14
};
你可以使用z.instanceof
来检查输入是否是一个类的实例。这对于验证从第三方库中导出的类的输入很有用。
class Test {
name: string;
}
const TestSchema = z.instanceof(Test);
const blob: any = "whatever";
TestSchema.parse(new Test()); // passes
TestSchema.parse(blob); // throws
Zod 还允许你定义 "函数模式(function schemas)"。这使得验证一个函数的输入和输出变得很容易,而不需要把验证代码和 "业务逻辑(business logic)"混在一起。
你可以用z.function(args, returnType)
创建一个函数模式。
const myFunction = z.function();
type myFunction = z.infer<typeof myFunction>;
// => ()=>unknown
定义输入和输出
const myFunction = z
.function()
.args(z.string(), z.number()) // 接受任意数量的参数
.returns(z.boolean());
type myFunction = z.infer<typeof myFunction>;
// => (arg0: string, arg1: number)=>boolean
提取输入和输出模式 你可以提取一个函数模式的参数和返回类型。
myFunction.parameters();
// => ZodTuple<[ZodString, ZodNumber]>
myFunction.returnType();
// => ZodBoolean
如果你的函数没有返回任何东西,你可以使用特殊的
z.void()
选项。这将让 Zod 正确地推断出无效返回的函数的类型。(无效返回的函数实际上可以返回未定义或空。)
函数模式有一个.implement()
方法,它接受一个函数并返回一个自动验证其输入和输出的新函数。
const trimmedLength = z
.function()
.args(z.string()) // accepts an arbitrary number of arguments
.returns(z.number())
.implement((x) => {
// TypeScript knows x is a string!
return x.trim().length;
});
trimmedLength("sandwich"); // => 8
trimmedLength(" asdf "); // => 4
如果你只关心验证输入,那就好了:
const myFunction = z
.function()
.args(z.string())
.implement((arg) => {
return [arg.length]; //
});
myFunction; // (arg: string)=>number[]
所有的 Zod 模式都包含一些方法。
.parse(data:unknown): T
给定任何 Zod 模式,你可以调用其.parse
方法来检查data
是否有效。如果是的话,就会返回一个带有完整类型信息的值。否则,会产生一个错误。
IMPORTANT: 在 Zod 2 和 Zod 1.11+中,
.parse
返回的值是你传入的变量的 deep clone 。这在[email protected] 和更早的版本中也是如此。
const stringSchema = z.string();
stringSchema.parse("fish"); // => returns "fish"
stringSchema.parse(12); // throws Error('Non-string type: number');
.parseAsync(data:unknown): Promise<T>
如果你使用异步的refinements或transforms(后面会有更多介绍),你需要使用.parseAsync
const stringSchema = z.string().refine(async (val) => val.length > 20);
const value = await stringSchema.parseAsync("hello"); // => hello
.safeParse(data:unknown): { success: true; data: T; } | { success: false; error: ZodError; }
如果你不希望 Zod 在验证失败时抛出错误,请使用.safeParse
。该方法返回一个包含成功解析的数据的对象,或者一个包含验证问题详细信息的 ZodError 实例。
stringSchema.safeParse(12);
// => { success: false; error: ZodError }
stringSchema.safeParse("billie");
// => { success: true; data: 'billie' }
结果是一个 discriminated union ,所以你可以非常方便地处理错误:
const result = stringSchema.safeParse("billie");
if (!result.success) {
// handle error then return
result.error;
} else {
// do something
result.data;
}
Alias:
.spa
一个异步版本的safeParse
。
await stringSchema.safeParseAsync("billie");
为方便起见,它已被别名为.spa
:
await stringSchema.spa("billie");
.refine(validator: (data:T)=>any, params?: RefineParams)
Zod 允许你通过 refinements 提供自定义验证逻辑。(关于创建多个问题和自定义错误代码等高级功能,见.superRefine
)。
Zod 被设计为尽可能地反映 TypeScript。但有许多所谓的 "细化类型",你可能希望检查不能在 TypeScript 的类型系统中表示。例如:检查一个数字是否是一个整数,或者一个字符串是否是一个有效的电子邮件地址。
例如,你可以用.refine
对任何 Zod 模式定义一个自定义验证检查:
const myString = z.string().refine((val) => val.length <= 255, {
message: "String can't be more than 255 characters",
});
⚠️ 精细化函数不应该抛出。相反,它们应该返回一个虚假的值来表示失败。
正如你所看到的,.refine
需要两个参数。
- 第一个是验证函数。这个函数接受一个输入(类型为
T
--模式的推断类型)并返回any
。任何真实的值都会通过验证。(在[email protected] 之前,验证函数必须返回一个布尔值。) - 第二个参数接受一些选项。你可以用它来定制某些错误处理行为:
type RefineParams = {
// 覆盖错误信息
message?: string;
// 附加到错误路径中
path?: (string | number)[];
// params对象,你可以用它来定制消息
// 在错误map中
params?: object;
};
对于高级情况,第二个参数也可以是一个返回RefineParams
的函数
z.string().refine(
(val) => val.length > 10,
(val) => ({ message: `${val} is not more than 10 characters` })
);
const passwordForm = z
.object({
password: z.string(),
confirm: z.string(),
})
.refine((data) => data.password === data.confirm, {
message: "Passwords don't match",
path: ["confirm"], // path of error
})
.parse({ password: "asdf", confirm: "qwer" });
因为你提供了一个路径(path)
参数,产生的错误将是:
ZodError {
issues: [{
"code": "custom",
"path": [ "confirm" ],
"message": "Passwords don't match"
}]
}
细化也可以是异步的:
const userId = z.string().refine(async (id) => {
// verify that ID exists in database
return true;
});
⚠️ 如果你使用异步细化,你必须使用.parseAsync
方法来解析数据! 否则 Zod 会抛出一个错误。
变换(transforms)和细化(refinements)可以交错进行:
z.string()
.transform((val) => val.length)
.refine((val) => val > 25);
.refine
方法实际上是在一个更通用的(也更啰嗦)的superRefine
方法之上的语法糖。下面是一个例子:
const Strings = z.array(z.string()).superRefine((val, ctx) => {
if (val.length > 3) {
ctx.addIssue({
code: z.ZodIssueCode.too_big,
maximum: 3,
type: "array",
inclusive: true,
message: "Too many items 😡",
});
}
if (val.length !== new Set(val).size) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `No duplicated allowed.`,
});
}
});
你可以随心所欲地添加问题(issues)。如果ctx.addIssue
在函数的执行过程中没有被调用,则验证通过。
通常情况下,细化总是创建具有ZodIssueCode.custom
错误代码的问题,但通过superRefine
你可以创建任何代码的任何问题。每个问题代码在错误处理指南 ERROR_HANDLING.md 中都有详细描述。
要在解析后转换数据,请使用transform
方法。
const stringToNumber = z.string().transform((val) => myString.length);
stringToNumber.parse("string"); // => 6
⚠️ 转化函数不得抛出。确保在转化器之前使用细化功能,以确保输入可以被转化器解析。
注意,上面的stringToNumber
是ZodEffects
子类的一个实例。它不是ZodString
的实例。如果你想使用ZodString
的内置方法(例如.email()
),你必须在进行任何转换 之前 应用这些方法。
const emailToDomain = z
.string()
.email()
.transform((val) => val.split("@")[1]);
emailToDomain.parse("[email protected]"); // => example.com
转换和细化可以交错进行:
z.string()
.transform((val) => val.length)
.refine((val) => val > 25);
转换也可以是异步的。
const IdToUser = z
.string()
.uuid()
.transform(async (id) => {
return await getUserById(id);
});
⚠️ 如果你的模式包含异步变换器,你必须使用.parseAsync()或.safeParseAsync()来解析数据。否则,Zod 将抛出一个错误。
你可以使用变换器来实现 Zod 中 "默认值 "的概念。
const stringWithDefault = z.string().default("tuna");
stringWithDefault.parse(undefined); // => "tuna"
你可以选择在.default
中传递一个函数,当需要生成默认值时,该函数将被重新执行:
const numberWithRandomDefault = z.number().default(Math.random);
numberWithRandomDefault.parse(undefined); // => 0.4413456736055323
numberWithRandomDefault.parse(undefined); // => 0.1871840107401901
numberWithRandomDefault.parse(undefined); // => 0.7223408162401552
一个方便的方法,返回一个模式的可选版本。
const optionalString = z.string().optional(); // string | undefined
// equivalent to
z.optional(z.string());
一个方便的方法,返回一个模式的可空版本。
const nullableString = z.string().nullable(); // string | null
// equivalent to
z.nullable(z.string());
一个方便的方法,用于返回模式的 "nullish "版本。空白模式将同时接受undefined
和null
。阅读更多关于 "nullish "的概念这里.
const nullishString = z.string().nullish(); // string | null | undefined
// equivalent to
z.string().nullable().optional();
一个方便的方法,为给定类型返回一个数组模式:
const nullableString = z.string().array(); // string[]
// equivalent to
z.array(z.string());
一个用于联合类型的方便方法。
z.string().or(z.number()); // string | number
// equivalent to
z.union([z.string(), z.number()]);
一个方便的方法,用于创建交叉类型。
z.object({ name: z.string() }).and(z.object({ age: z.number() })); // { name: string } & { age: number }
// equivalent to
z.intersection(z.object({ name: z.string() }), z.object({ age: z.number() }));
你可以用z.infer<typeof mySchema>
提取任何模式的 TypeScript 类型。
const A = z.string();
type A = z.infer<typeof A>; // string
const u: A = 12; // TypeError
const u: A = "asdf"; // compiles
在现实中,每个 Zod 模式实际上都与两种类型相关:一个输入和一个输出。对于大多数模式(例如z.string()
),这两种类型是相同的。但是一旦你把转换添加到混合中,这两个值就会发生分歧。例如,z.string().transform(val => val.length)
的输入为string
,输出为number
。
你可以像这样分别提取输入和输出类型:
const stringToNumber = z.string().transform((val) => val.length);
// ⚠️ Important: z.infer返回OUTPUT类型!
type input = z.input<stringToNumber>; // string
type output = z.output<stringToNumber>; // number
// equivalent to z.output!
type inferred = z.infer<stringToNumber>; // number
Zod 提供了一个名为 ZodError
的错误子类。ZodErrors 包含一个issues
数组,包含关于验证问题的详细信息。
const data = z
.object({
name: z.string(),
})
.safeParse({ name: 12 });
if (!data.success) {
data.error.issues;
/* [
{
"code": "invalid_type",
"expected": "string",
"received": "number",
"path": [ "name" ],
"message": "Expected string, received number"
}
] */
}
你可以使用.format()
方法将这个错误转换为一个嵌套对象。
data.error.format();
/* {
name: { _errors: [ 'Expected string, received number' ] }
} */
关于可能的错误代码和如何定制错误信息的详细信息,请查看专门的错误处理指南: ERROR_HANDLING.md
还有一些其他广泛使用的验证库,但它们都有一定的设计局限性,使开发者的体验不理想。
不支持静态类型推理 😕
https://github.com/jquense/yup
Yup 是一个全功能的库,首先用 vanilla JS 实现,后来又用 TypeScript 重写。
不同之处
- 支持铸造和转换
- 所有的对象字段默认都是可选的
- 缺少方法: (partial, deepPartial)
- 缺少 promise 模式
- 缺少 function 模式
- 缺少联合和交叉模式
https://github.com/gcanti/io-ts
io-ts 是 gcanti 的一个优秀库。io-ts 的 API 极大地启发了 Zod 的设计。
根据我们的经验,在许多情况下,io-ts 优先考虑功能编程的纯洁性,而不是开发者的经验。这是一个有效的和令人钦佩的设计目标,但它使 io-ts 特别难以集成到一个现有的程序化或面向对象的代码库中。例如,考虑如何在 io-ts 中定义一个具有可选属性的对象:
import * as t from "io-ts";
const A = t.type({
foo: t.string,
});
const B = t.partial({
bar: t.number,
});
const C = t.intersection([A, B]);
type C = t.TypeOf<typeof C>;
// returns { foo: string; bar?: number | undefined }
你必须在不同的对象验证器中定义必需的和可选的道具,通过t.partial
(它将所有属性标记为可选)传递选项,然后用t.intersection
组合它们。
考虑在 Zod 中的对应关系:
const C = z.object({
foo: z.string(),
bar: z.number().optional(),
});
type C = z.infer<typeof C>;
// returns { foo: string; bar?: number | undefined }
这种更具声明性的 API 使模式定义更加简明。
io-ts
也需要使用 gcanti 的函数式编程库fp-ts
来解析结果和处理错误。对于希望严格保持代码库功能的开发者来说,这是另一个极好的资源。但是,依赖fp-ts
必然带来大量的知识开销;开发人员必须熟悉函数式编程的概念和fp-ts
的命名,才能使用这个库。
- 支持具有序列化和反序列化转换功能的编解码器
- 支持 branded types
- 支持高级函数式编程、高级类型、
fp-ts
。compatibility - 缺少的方法:(pick, omit, partial, deepPartial, merge, extend)
- 缺少具有正确类型的非空数组(`[T, ...T[]])。
- 缺少 promise 模式
- 缺少 function 模式
https://github.com/pelotom/runtypes
良好的类型推理支持,但对象类型屏蔽的选项有限(没有.pick
,.omit
,.extend
,等等)。不支持 Record
(他们的 Record
等同于 Zod 的 object
)。他们确实支持 branded 和 readonly 类型,而 Zod 不支持。
- 支持 "模式匹配(pattern matching)":分布在联合体上的计算属性
- 支持只读类型
- 缺少的方法:(deepPartial, merge)
- 缺少具有适当类型的非空数组(`[T, ...T[]])。
- 缺少 promise 模式
- 缺少错误定制功能
https://github.com/sindresorhus/ow
Ow 专注于函数输入验证。它是一个使复杂的断言语句容易表达的库,但它不能让你解析未定型的数据。他们支持更多的类型;Zod 与 TypeScript 的类型系统几乎是一对一的映射,而 Ow 可以让你验证几个高度特定的类型(例如int32Array
,见他们的 README 中的完整列表)。
如果你想验证函数输入,请在 Zod 中使用函数模式! 这是一个更简单的方法,让你可以重复使用一个函数类型声明,而不需要重复自己(即在每个函数的开头复制粘贴一堆 ow assertions)。此外,Zod 还可以让你验证你的返回类型,所以你可以确保不会有任何意外的数据传递到下游。
查看更新日志点击 CHANGELOG.md