Skip to content

Latest commit

 

History

History
2051 lines (1540 loc) · 58 KB

README_ZH.md

File metadata and controls

2051 lines (1540 loc) · 58 KB

Zod logo

Zod

利用静态类型推断进行 TypeScript 优先模式验证
https://zod.dev


Zod CI status Created by Colin McDonnell License npm stars discord server

文档   •   Discord   •   NPM   •   讨论   •   @colinhacks   •   tRPC


內容

什么是 Zod

Zod 是一个 TypeScript 优先的模式声明和验证库。我使用术语 "模式" 来广义地指任何数据类型,从简单的 字符串 到复杂的嵌套对象。

Zod 围绕尽可能友好的开发体验而设计。其目的是消除重复的类型声明。使用 Zod,你只需声明 一次 验证器,Zod 就会自动推断出静态 TypeScript 类型。将简单类型组合成复杂的数据结构非常容易。

其他一些重要方面:

  • 零依赖
  • 适用于 Node.js 和所有现代浏览器
  • 小巧: 压缩后仅 8kb
  • 不可变: 方法 (如 .optional() ) 返回一个新的实例
  • 简洁的、可链式调用的接口
  • 函数式方法: 解析,不验证
  • 也可用于纯 JavaScript! 你不需要使用 TypeScript。

赞助

我们感谢并鼓励任何级别的赞助。Zod 是由一个单独的开发者维护的 (hi!)。对于个人开发者,可以考虑一杯咖啡级别。如果你使用 Zod 建立了一个付费产品,可以考虑领奖台级别

黄金

Astro
Astro
astro.build

Astro is a new kind of static
site builder for the modern web.
Powerful developer experience meets
lightweight output.


Glow Wallet
glow.app

Your new favorite
Solana wallet.


Deletype
deletype.com

白银


Snaplet
snaplet.dev
Marcato Partners
Marcato Partners
marcatopartners.com
Trip
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 logo
Avana Wallet
avanawallet.com
Solana non-custodial wallet

要在这里看到你的名字 + Twitter + 網站 , 请在FreelancerConsultancy赞助 Zod .

生态体系

有越来越多的工具是建立在 Zod 之上或原生支持 Zod 的! 如果你在 Zod 的基础上建立了一个工具或库,请在Twitter 或者 Discussion上告诉我。我会把它添加到下面,并在推特上发布。

  • tRPC: 在没有 GraphQL 的情况下建立端到端的类型安全 API
  • react-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 安装

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

字面量(literal)

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" });

ISO 日期

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

IP 地址

默认情况下,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

Numbers

在创建数字模式时,你可以自定义某些错误信息

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" });

Dates

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!" });

Objects

// 所有属性都是默认需要的
const Dog = z.object({
  name: z.string(),
  age: z.number(),
});

// 像这样提取推断出的类型
type Dog = z.infer<typeof Dog>;

// 相当于:
type Dog = {
  name: string;
  age: number;
};

.shape

使用.shape来访问特定键的模式。

Dog.shape.name; // => string schema
Dog.shape.age; // => number schema

.extend

你可以用.extend方法在对象模式中添加额外的字段。

const DogWithBreed = Dog.extend({
  breed: z.string(),
});

你可以使用.extend来覆盖字段! 要小心使用这种方式!

.merge

相当于 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 的全面模式。

.pick/.omit

受 TypeScript 内置的PickOmit工具类型的启发,所有 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[] }

.partial

受 TypeScript 内置的实用类型Partial的启发, .partial 方法使所有属性都是可选的。

从这个对象开始:

const user = z.object({
  username: z.string(),
});
// { username: string }

我们可以创建一个 Partial 版本:

const partialUser = user.partial();
// { username?: string | undefined }

.deepPartial

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 只在对象模式的直接层次中按预期工作。嵌套的对象模式不能是可选的,不能是空的,不能包含细化,不能包含转换,等等...

未被识别的 keys

默认情况下,Zod 对象的模式在解析过程中会剥离出未被识别的 keys

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

person.parse({
  name: "bob dylan",
  extraKey: 61,
});
// => { name: "bob dylan" }
// extraKey已经被剥离

.passthrough

相反,如果你想通过未知的 keys,使用.passthrough()

person.passthrough().parse({
  name: "bob dylan",
  extraKey: 61,
});
// => { name: "bob dylan", extraKey: 61 }

.strict

你可以用.strict()禁止 未知键。如果输入中存在任何未知的 keys,Zod 将抛出一个错误。

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

person.parse({
  name: "bob dylan",
  extraKey: 61,
});
// => throws ZodError

.strip

你可以使用.strip方法将一个对象模式重置为默认行为(剥离未识别的 keys)。

.catchall

你可以将一个 "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)"。

Arrays

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

如果你想确保一个数组至少包含一个元素,使用 .nonempty().

const nonEmptyStrings = z.string().array().nonempty();
// 现在推断的类型是
// [string, ...string[]]

nonEmptyStrings.parse([]); // throws: "Array cannot be empty"
nonEmptyStrings.parse(["Ariana Grande"]); // passes

.min/.max/.length

z.string().array().min(5); // 必须包含5个或更多元素
z.string().array().max(5); // 必须包含5个或更少元素
z.string().array().length(5); // 必须正好包含5个元素

.nonempty()不同,这些方法不会改变推断的类型。

Unions

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());

Discriminated unions

判别联合模式是指联合类型有一个特定键,根据该键值命中对应的对象模式。

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]);

Optionals

你可以用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 };

.unwrap

const stringSchema = z.string();
const optionalString = stringSchema.optional();
optionalString.unwrap() === stringSchema; // true

Nullables

类似地,你可以这样创建 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

.unwrap

const stringSchema = z.string();
const nullableString = stringSchema.nullable();
nullableString.unwrap() === stringSchema; // true

Records

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 中没有数字键这回事。

Maps

const stringNumberMap = z.map(z.string(), z.number());

type StringNumberMap = z.infer<typeof stringNumberMap>;
// type StringNumber = Map<string, number>

Sets

const numberSet = z.set(z.number());
type numberSet = z.infer<typeof numberSet>;
// Set<number>

Enums

在 Zod 中,有两种方法来定义枚举。

Zod enums

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"]);

Native enums

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

Intersections

交叉类型对于创建 "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实例,它缺乏像pickomit这样的常用对象方法。

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

与数组不同,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 }]

Recursive types

你可以在 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 type

如果你想验证任何 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的建议。

Cyclical objects

尽管支持递归模式,但将一个循环数据传入 Zod 会导致无限循环。

Promises

const numberPromise = z.promise(z.number());

"Parsing"的工作方式与 promise 模式有点不同。验证分两部分进行:

  1. Zod 同步检查输入是否是 Promise 的实例(即一个具有.then.catch方法的对象)。
  2. 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
};

Instanceof

你可以使用z.instanceof来检查输入是否是一个类的实例。这对于验证从第三方库中导出的类的输入很有用。

class Test {
  name: string;
}

const TestSchema = z.instanceof(Test);

const blob: any = "whatever";
TestSchema.parse(new Test()); // passes
TestSchema.parse(blob); // throws

Function schemas

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[]

ZodType: methods and properties

所有的 Zod 模式都包含一些方法。

.parse

.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

.parseAsync(data:unknown): Promise<T>

如果你使用异步的refinementstransforms(后面会有更多介绍),你需要使用.parseAsync

const stringSchema = z.string().refine(async (val) => val.length > 20);
const value = await stringSchema.parseAsync("hello"); // => hello

.safeParse

.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;
}

.safeParseAsync

Alias: .spa

一个异步版本的safeParse

await stringSchema.safeParseAsync("billie");

为方便起见,它已被别名为.spa:

await stringSchema.spa("billie");

.refine

.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",
});

⚠️ 精细化函数不应该抛出。相反,它们应该返回一个虚假的值来表示失败。

Arguments

正如你所看到的,.refine需要两个参数。

  1. 第一个是验证函数。这个函数接受一个输入(类型为T--模式的推断类型)并返回any。任何真实的值都会通过验证。(在[email protected] 之前,验证函数必须返回一个布尔值。)
  2. 第二个参数接受一些选项。你可以用它来定制某些错误处理行为:
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` })
);

Customize error path

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"
  }]
}

Asynchronous refinements

细化也可以是异步的:

const userId = z.string().refine(async (id) => {
  // verify that ID exists in database
  return true;
});

⚠️ 如果你使用异步细化,你必须使用.parseAsync方法来解析数据! 否则 Zod 会抛出一个错误。

Relationship to transforms

变换(transforms)和细化(refinements)可以交错进行:

z.string()
  .transform((val) => val.length)
  .refine((val) => val > 25);

.superRefine

.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

要在解析后转换数据,请使用transform方法。

const stringToNumber = z.string().transform((val) => myString.length);
stringToNumber.parse("string"); // => 6

⚠️ 转化函数不得抛出。确保在转化器之前使用细化功能,以确保输入可以被转化器解析。

Chaining order

注意,上面的stringToNumberZodEffects子类的一个实例。它不是ZodString的实例。如果你想使用ZodString的内置方法(例如.email()),你必须在进行任何转换 之前 应用这些方法。

const emailToDomain = z
  .string()
  .email()
  .transform((val) => val.split("@")[1]);

emailToDomain.parse("[email protected]"); // => example.com

Relationship to refinements

转换和细化可以交错进行:

z.string()
  .transform((val) => val.length)
  .refine((val) => val > 25);

Async transformations

转换也可以是异步的。

const IdToUser = z
  .string()
  .uuid()
  .transform(async (id) => {
    return await getUserById(id);
  });

⚠️ 如果你的模式包含异步变换器,你必须使用.parseAsync()或.safeParseAsync()来解析数据。否则,Zod 将抛出一个错误。

.default

你可以使用变换器来实现 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

.optional

一个方便的方法,返回一个模式的可选版本。

const optionalString = z.string().optional(); // string | undefined

// equivalent to
z.optional(z.string());

.nullable

一个方便的方法,返回一个模式的可空版本。

const nullableString = z.string().nullable(); // string | null

// equivalent to
z.nullable(z.string());

.nullish

一个方便的方法,用于返回模式的 "nullish "版本。空白模式将同时接受undefinednull。阅读更多关于 "nullish "的概念这里.

const nullishString = z.string().nullish(); // string | null | undefined

// equivalent to
z.string().nullable().optional();

.array

一个方便的方法,为给定类型返回一个数组模式:

const nullableString = z.string().array(); // string[]

// equivalent to
z.array(z.string());

.or

一个用于联合类型的方便方法。

z.string().or(z.number()); // string | number

// equivalent to
z.union([z.string(), z.number()]);

.and

一个方便的方法,用于创建交叉类型。

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() }));

Type inference

你可以用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

What about transforms?

在现实中,每个 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

Errors

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"
      }
  ] */
}

Error formatting

你可以使用.format()方法将这个错误转换为一个嵌套对象。

data.error.format();
/* {
  name: { _errors: [ 'Expected string, received number' ] }
} */

关于可能的错误代码和如何定制错误信息的详细信息,请查看专门的错误处理指南: ERROR_HANDLING.md

Comparison

还有一些其他广泛使用的验证库,但它们都有一定的设计局限性,使开发者的体验不理想。

Joi

https://github.com/hapijs/joi

不支持静态类型推理 😕

Yup

https://github.com/jquense/yup

Yup 是一个全功能的库,首先用 vanilla JS 实现,后来又用 TypeScript 重写。

不同之处

  • 支持铸造和转换
  • 所有的对象字段默认都是可选的
  • 缺少方法: (partial, deepPartial)
  • 缺少 promise 模式
  • 缺少 function 模式
  • 缺少联合和交叉模式

io-ts

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 模式

Runtypes

https://github.com/pelotom/runtypes

良好的类型推理支持,但对象类型屏蔽的选项有限(没有.pick.omit.extend,等等)。不支持 Record(他们的 Record 等同于 Zod 的 object )。他们确实支持 branded 和 readonly 类型,而 Zod 不支持。

  • 支持 "模式匹配(pattern matching)":分布在联合体上的计算属性
  • 支持只读类型
  • 缺少的方法:(deepPartial, merge)
  • 缺少具有适当类型的非空数组(`[T, ...T[]])。
  • 缺少 promise 模式
  • 缺少错误定制功能

Ow

https://github.com/sindresorhus/ow

Ow 专注于函数输入验证。它是一个使复杂的断言语句容易表达的库,但它不能让你解析未定型的数据。他们支持更多的类型;Zod 与 TypeScript 的类型系统几乎是一对一的映射,而 Ow 可以让你验证几个高度特定的类型(例如int32Array,见他们的 README 中的完整列表)。

如果你想验证函数输入,请在 Zod 中使用函数模式! 这是一个更简单的方法,让你可以重复使用一个函数类型声明,而不需要重复自己(即在每个函数的开头复制粘贴一堆 ow assertions)。此外,Zod 还可以让你验证你的返回类型,所以你可以确保不会有任何意外的数据传递到下游。

Changelog

查看更新日志点击 CHANGELOG.md