Skip to content

Latest commit

 

History

History
1736 lines (1276 loc) · 49.1 KB

README_ZH.md

File metadata and controls

1736 lines (1276 loc) · 49.1 KB

Zod

Created by Colin McDonnell License npm stars coverage

⭐️ smash that star button ⭐️

如果你喜欢 Zod,你会喜欢我的新库 tRPC. 这是一种建立端到端类型安全 API 的方法,无需 GraphQL 或代码生成 请在trpc.io上查看他.


二零二一年五月十七日: Zod v3 已经进入稳定版!

查看 迁移指南 进行升级.

以前的版本:

新功能

  • 更容易的引入: 你现在可以像import { z } from 'zod';那样导入 Zod,而不是使用import * as语法。
  • 结构化的错误消息. 对 ZodError 使用.format()方法,将错误转换为强类型的嵌套对象: format method
  • 更容易的联合类型. 使用 ZodType(所有 Zod 模式的基类)的or方法,可以轻松创建联合类型,如 z.string().or(z.number())
  • 更容易的交叉类型. 对 ZodType(所有 Zod 模式的基类)使用and方法,可以轻松创建交叉类型
  • 全局错误定制. 使用z.setErrorMap(myErrorMap)来全面定制 Zod 产生的错误信息: setErrorMap
  • Maps and sets. Zod 现在支持 MapSet 模式。
  • 可选的和可忽略的解包. ZodOptional 和 ZodNullable 现在有一个.unwrap()方法来检索它们所包裹的模式。
  • A new implementation of transformers. 请参阅迁移指南部分以了解语法的变化。

內容

什么是 Zod

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

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

其他一些重要方面:

  • 零依赖
  • 可以工作在浏览器和 Node.js
  • 小巧: 8kb minified + zipped
  • 不可变: 方法(即 .optional() 返回一个新的实例
  • 简洁的、可连锁的接口
  • 功能性方法: 解析,不验证
  • 也可用于普通的 JavaScript! 你不需要使用 TypeScript。

赞助

我们感谢并鼓励任何级别的赞助。Zod 是由一个单独的开发者维护的 (hi!). 对于个人开发者来说,可以考虑一杯咖啡级别. 如果你使用 Zod 建立了一个付费产品,可以考虑初创企业级别. 你可以在以下网站上了解更多关于等级的信息 github.com/sponsors/colinhacks.

贊助者


Deletype
https://deletype.com/

Kevin Simper
@kevinsimper

Brandon Bayer
@flybayer, creator of Blitz.js

Bamboo Creative
https://bamboocreative.nz

Jeremy Banks
github.com/jeremyBanks
Marcato Partners
Marcato Partners
marcatopartners.com

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

安装

安装 Zod v3:

npm install zod

⚠️ 重要提示:你必须在你的tsconfig.json中启用strict模式。这是所有 TypeScript 项目的最佳实践。

// tsconfig.json
{
  // ...
  "compilerOptions": {
    // ...
    "strict": true
  }
}

TypeScript 的要求

  • Zod 3.x requires TypeScript 4.1+
  • Zod 2.x requires TypeScript 3.7+
  • Zod 1.x requires TypeScript 3.3+

生态系统

有越来越多的工具是建立在 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 服务

基本用法

创建一个简单的字符串模式

import { z } from "zod";

// 创建一个字符串的模式
const mySchema = z.string();
mySchema.parse("tuna"); // => "tuna"
mySchema.parse(12); // => throws ZodError

创建一个 Object 模式

import { z } from "zod";

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

User.parse({ username: string });

// 抽出推断的类型
type User = z.infer<typeof User>;
// { username: string }

定义模式

基本原理

import { z } from "zod";

// 原始值
z.string();
z.number();
z.bigint();
z.boolean();
z.date();

// 空类型
z.undefined();
z.null();
z.void(); // 接受null或undefined

// 全能类型
// 允许 any value
z.any();
z.unknown();

// never 类型
// 允许没有 values
z.never();

字面意义

const tuna = z.literal("tuna");
const twelve = z.literal(12);
const tru = z.literal(true);

// 检索字面意义的值
tuna.value; // "tuna"

目前在 Zod 中不支持 Date 或 bigint 字面。如果你有这个功能的用例,请提交一个 Issue。

Strings

Zod 包括一些针对字符串的验证。

z.string().max(5);
z.string().min(5);
z.string().length(5);
z.string().email();
z.string().url();
z.string().uuid();
z.string().regex(regex);

// 已废弃,等同于 .min(1)
z.string().nonempty();

查看 validator.js,了解其他一些有用的字符串验证函数

自定义错误信息

你可以选择传入第二个参数来提供一个自定义的错误信息。

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().uuid({ message: "Invalid UUID" });

Numbers

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(); // 值必须是一个整数

z.number().positive(); //     > 0
z.number().nonnegative(); //  >= 0
z.number().negative(); //     < 0
z.number().nonpositive(); //  <= 0

z.number().multipleOf(5); // x % 5 === 0

你可以选择传入第二个参数来提供一个自定义的错误信息。

z.number().max(5, { message: "this👏is👏too👏big" });

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

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 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'

自动补全

为了获得 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 值,你可以使用下面的片段。

type Literal = boolean | null | number | string;
type Json = Literal | { [key: string]: Json } | Json[];
const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
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 。这在zod@1.4 和更早的版本中也是如此。

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。任何真实的值都会通过验证。(在zod@1.6.2 之前,验证函数必须返回一个布尔值。)
  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("colinhacks@example.com"); // => 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().optional().nullable();

.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