@badrap/valita (英语)

一个用于验证和剖析结构对象的 TypeScript 库。 API为 严重 受影响 Zod's (Zod's) (美国英语) 优秀的API,而执行方面则旨在取得令人印象深刻的业绩 简单运行类型。 。 。 .
const vehicle = v.union(
v.object({ type: v.literal("plane"), airline: v.string() }),
v.object({ type: v.literal("train") }),
v.object({ type: v.literal("automobile"), make: v.string() }),
);
vehicle.parse({ type: "bike" });
// ValitaError: invalid_literal at .type (expected "plane", "train" or "automobile")
[注] 虽然这个软件包仍在发展,但我们目前不接受任何新的功能请求或建议. 请使用问题跟踪器来获取错误报告和安全问题,我们高度重视和欢迎这些问题。 谢谢你的理解
目标和非目标
目标
- 输入验证和解析: 库的基本目标是确保可能不是来自可信来源的来入数据与预定格式相匹配.
- 最小化:提供精简和集中的图书馆,仅提供基本内容.
- 扩展性:允许用户创建符合特定验证情景的自有验证器和解析器.
非目标
- 数据定义:库被设计为在输入程序时验证和剖析输入数据,而不是作为获取输入后定义程序内部所有类型的详尽工具.
- 扩展内建格式: 库不优先排出框外大量内置验证格式.
- 同步解析:同步操作在本库范围之外.
安装
为节点.js
pnpm add @badrap/valita
为德诺
deno add @badrap/valita
API 参考
本节概述所有验证方法.
初级类型
先讲基础 像每一个验证库一样,我们支持所有原始类型,如字符串,数字,布尔等. 比如说 v.string() 原始值可以像这样用来检查某些输入值是否为字符串 :
import * as v from "@badrap/valita";
const t = v.string();
t.parse("Hello, World!");
// "Hello, World!"
尝试解析任何非字符串而你得到一个错误:
t.parse(1);
// ValitaError: invalid_type at . (expected string)
.parse(...) 键入方式为它接受任何类型的输入值,但在成功时返回一个适当的输入值:
const u: unknown = "Hello, World!";
// TypeScript type checking is happy with this!
const s: string = t.parse(u);
原始类型有:
v.string(): 请检查该值是一个字符串.v.number(): 检查该值是一个数字(即:.typeof value === "number",包括NaN和±无穷).v.bigint(): 检查该值是 二进制 (即说.typeof value === "bigint"(中文(简体) )v.boolean(): 检查该值是否为布尔值 .v.null(): 请检查该值是null。 。 。 .v.undefined(): 请检查该值是undefined。 。 。 .
文字类型
有时知道某个值是否属于某一类型是不够的. 我们可以使用 .literal() 用于检查实际值的方法, 如检查字符串是否相同 "red", (中文(简体) ), "green" 或者说 "blue" 不只是任何绳子.
const rgb = v.union(v.literal("red"), v.literal("green"), v.literal("blue"));
rgb.parse("green");
// "green"
rgb.parse("magenta");
// ValitaError: invalid_literal at . (expected "red", "green" or "blue")
我们也可以用这个来检查具体数字,比值或布尔值:
v.literal(1); // must be number 1
v.literal(1n); // must be bigint 1n
v.literal(true); // must be true
对于更复杂的值,您可以使用 .assert()- 办法 检查出来 自定义验证器 来了解更多.
允许任意/ 全部禁用
Valita不包含相当于 TypeScript 的内置 any 类型。 不过, v.unknown() 类似于类型脚本 unknown 类型,并可用于接受任何输入值:
const u = v.unknown();
u.parse(1);
// 1
反之则 v.unknown() 实值 v.never() 对每个值都失败了 这类似于 TypeScript 的 never 类型.
const n = v.never();
n.parse(1);
// ValitaError: invalid_type at . (expected nothing)
自己一个人 v.unknown() 财务报告和审定财务报表 v.never() 并不十分有用,但它们与综合类型(例如: 对象类型。 。 。 .
对象类型
验证器可以进一步与更大的,任意复杂的验证器结合. 一个这样的组合是 v.object(...),用于检查输入值是一个具有某些命名属性的对象,这些属性具有特定类型.
const o = v.object({
company: v.string(),
// Nested objects work fine too!
address: v.object({
city: v.string(),
country: v.string(),
}),
});
o.parse({
name: "Acme Inc.",
address: { city: "Springfield", country: "Freedomland" },
});
// {
// name: "Acme Inc.",
// address: { city: "Springfield", country: "Freedomland" },
// }
o.parse({ name: "Acme Inc." });
// ValitaError: missing_value at .address (missing value)
o.parse({
name: "Acme Inc.",
ceo: "Wiley E. Coyote",
address: { city: "Springfield", country: "Freedomland" },
});
// ValitaError: unrecognized_keys at . (unrecognized key "ceo")
如上所述,意外的密钥 "ceo" 默认禁止。 默认可用 解析模式。 。 。 .
解析模式
默认 v.object(...) 遇到有出乎意料的密钥的对象时会丢出错误。 这种行为可以通过明确通过 解析模式 改为 .parse(...)数字 :
const o = v.object({
name: v.string(),
});
// Strip away the extra keys
o.parse({ name: "Acme Inc.", ceo: "Wiley E. Coyote" }, { mode: "strip" });
// { name: "Acme Inc." }
// Pass the extra keys through as-is
o.parse({ name: "Acme Inc.", ceo: "Wiley E. Coyote" }, { mode: "passthrough" });
// { name: "Acme Inc.", ceo: "Wiley E. Coyote" }
// Forbid extra keys. This is the default.
o.parse({ name: "Acme Inc.", ceo: "Wiley E. Coyote" }, { mode: "strict" });
// ValitaError: unrecognized_keys at . (unrecognized key "ceo")
可能的价值是:
{ mode: "strict" }: 无效的额外密钥。 这是默认.{ mode: "strip" }: 别在额外的密钥上出错 - 而不是把它们从输出对象中剥离出.{ mode: "passthrough" }:只是忽略了多余的钥匙,假装你没有看到.
解析模式适用于您的验证等级, 甚至适用于嵌入对象 .
const o = v.object({
company: v.object({
name: v.string(),
}),
});
o.parse(
{
company: { name: "Acme Inc.", ceo: "Wiley E. Coyote" },
greeting: "Hello!",
},
{ mode: "strip" },
);
// { company: { name: "Acme Inc." } }
暂停属性记录( R)
有时您可能想要在定义的密钥之外允许额外的密钥. 你可以用这个 .rest(...),另外要求额外的密钥具有特定类型的值:
const o = v
.object({
name: v.string(),
age: v.number(),
})
.rest(v.string());
o.parse({ name: "Example McExampleface", age: 42, socks: "yellow" });
// { name: "Example McExampleface", age: 42, socks: "yellow" }
o.parse({ name: "Example McExampleface", age: 42, numberOfDogs: 2 });
// ValitaError: invalid_type at .numberOfDogs (expected string)
这个 .rest(...) 方法对于允许或禁止对象结构中某个特定部分的额外密钥也是有用的,无论解析模式如何.
const lenient = v.object({}).rest(v.unknown()); // *Always* allow extra keys
lenient.parse({ socks: "yellow" }, { mode: "strict" });
// { socks: "yellow" }
const strict = v.object({}).rest(v.never()); // *Never* allow extra keys
strict.parse({ socks: "yellow" }, { mode: "strip" });
// ValitaError: invalid_type at .socks (expected nothing)
因为总是允许 完全任意的数量的财产, v.record(...) 简称为 v.object({}).rest(...)。这类似于 Record<string, ...> 类型脚本.
const r = v.record(v.number());
r.parse({ a: 1, b: 2 });
// { a: 1, b: 2 }
r.parse({ a: 1, b: "hello" });
// ValitaError: invalid_type at .b (expected number)
允许收集任何类型的属性, v.record() 是一个缩写 v.record(v.unknown())。 。 。 .
const r = v.record();
r.parse({ a: 1, b: 2 });
// { a: 1, b: 2 }
r.parse({ a: 1, b: "hello" });
// { a: 1, b: "hello" }
// The input still has to be an object.
r.parse([]);
// ValitaError: invalid_type at . (expected object)
可选属性
一个常见的API模式是一些对象字段是 选项,即它们可能完全缺失或被设定为 undefined。您可以通过注释这些密钥来让一些密钥缺失 .optional()。 。 。 .
const person = v.object({
name: v.string(),
// Not everyone filled in their theme song
themeSong: v.string().optional(),
});
person.parse({ name: "Jane Doe", themeSong: "Never gonna give you up" });
// { name: "Jane Doe", themeSong: "Never gonna give you up" }
person.parse({ name: "Jane Doe" });
// { name: "Jane Doe" }
person.parse({ name: "Jane Doe", themeSong: undefined });
// { name: "Jane Doe", themeSong: undefined }
作为独立的解析器,可选选项被接受 undefined 除被包裹的类型外:
const t = v.string().optional();
t.parse("Hello, World!");
// "Hello, World!";
t.parse(undefined);
// undefined
一个可选函数可用于替换缺失或未定义的值,取而代之的是其他一些默认值:
const person = v.object({
name: v.string(),
// Set a sensible default for those unwilling to fill in their theme song
themeSong: v.string().optional(() => "Tribute"),
});
person.parse({ name: "Jane Doe", themeSong: "Never gonna give you up" });
// { name: "Jane Doe", themeSong: "Never gonna give you up" }
person.parse({ name: "Jane Doe" });
// { name: "Jane Doe", themeSong: "Tribute" }
person.parse({ name: "Jane Doe", themeSong: undefined });
// { name: "Jane Doe", themeSong: "Tribute" }
默认函数被重新评价每个缺失或未定义的值,以避免在不同解析值之间意外共享对象或数组等可变的默认值.
矩阵类型
这个 v.array(...) 组合符可以用来检查该值是一个数组,并且其项目有特定类型。 经验证的数组可能任意长度,包括空数组.
const a = v.array(v.object({ name: v.string() }));
a.parse([{ name: "Acme Inc." }, { name: "Evil Corporation" }]);
// [{ name: "Acme Inc." }, { name: "Evil Corporation" }]
a.parse([]);
// []
a.parse({ 0: { name: "Acme Inc." } });
// ValitaError: invalid_type at . (expected array)
因为允许任何阵列, v.array() 是一个缩写 v.array(v.unknown())。 。 。 .
const a = v.array();
a.parse(["foo", 1]);
// ["foo", 1]
a.parse([]);
// []
// Only arrays are allowed, though.
a.parse({ 0: { name: "Acme Inc." } });
// ValitaError: invalid_type at . (expected array)
Tuple 类型
尽管JavaScript没有Tuple值(是吗?),许多API用数组来模拟它们. 例如,如果我们需要编码两个数字之间的范围,我们可能选择 type Range = [number, number] 作为数据类型。 从JavaScript的观点来看,它只是一个阵列,但TypeScript知道每个位置的价值以及阵列 必须 有两个条目.
我们可以表达这种类型 v.tuple(...)数字 :
const range = v.tuple([v.number(), v.number()]);
range.parse([1, 2]);
// [1, 2]
range.parse([200, 2]);
// [200, 2]
range.parse([1]);
// ValitaError: invalid_length at . (expected an array with 2 item(s))
range.parse([1, 2, 3]);
// ValitaError: invalid_length at . (expected an array with 2 item(s))
range.parse([1, "2"]);
// ValitaError: invalid_type at .1 (expected number)
Tuples可以与其他 Tuples相接来创造出新的 Tuple 类型, 并用偶数组来创建 二重管类型。 。 。 .
const twoNumbers = v.tuple([v.number(), v.number()]); // Type<[number number]>
const twoStrings = v.tuple([v.string(), v.string()]); // Type<[string, string]>
const booleans = v.array(v.boolean()); // Type<boolean[]>
twoNumbers.concat(twoStrings);
// Type<[number, number, string, string]>
twoNumbers.concat(booleans);
// Type<[number, number, ...boolean[]]>
booleans.concat(twoStrings);
// Type<[...boolean[], string, string]>
twoNumbers.concat(booleans).concat(twoStrings);
// Type<[number, number, ...boolean[], string, string]>
两个可变长的扭矩或阵列不能被调和,不过,所以这是一个类型级错误,在运行时也会抛出一个错误:
v.tuple([]).concat(v.array()).concat(v.array());
// TypeError: can not concatenate two variadic types
联盟类型
工会类型是一种价值,可以有几种不同的代表。 想象一下我们有种价值 Shape 可以是三角形、圆形或正方形:
const triangle = v.object({ type: v.literal("triangle") });
const square = v.object({ type: v.literal("square") });
const circle = v.object({ type: v.literal("circle") });
const shape = v.union(triangle, square, circle);
shape.parse({ type: "triangle" });
// { type: "triangle" }
shape.parse({ type: "heptagon" });
// ValitaError: invalid_literal at .type (expected "triangle", "square" or "circle")
请注意,尽管在此例子中,所有表述都是对象,并拥有共有财产 type没必要 每种代表可以有完全不同的基础类型.
const primitive = v.union(v.number(), v.string(), v.boolean());
primitive.parse("Hello, World!");
// "Hello, World!"
primitive.parse({});
// ValitaError: invalid_type at . (expected number, string or boolean)
可忽略类型
在与API或数据库合作时,有些类型可能无效. 这个 t.nullable() shortwand 返回一个等于 v.union(v.null(), t)。 。 。 .
// type name = null | string
const name = v.string().nullable();
// Passes
name.parse("Acme Inc.");
// Passes
name.parse(null);
与 .optional(),用于替换 null 带有其他默认值的值 :
const name = v.string().nullable(() => "Unknown");
name.parse("Jane Doe");
// "Jane Doe"
name.parse(null);
// "Unknown"
默认函数每一次都会重新评价 null 以避免在不同解析值之间意外共享对象或数组等可变默认值.
递归类型
有些类型可能包含任意筑巢,比如: type T = string | T[]。我们可以用 .lazy(...)。 。 。 .
注意TypeScript不能推断回转函数的类型. 这就是为什么 v.lazy(...) 验证符需要明确输入 v.Type<T>。 。 。 .
type T = string | T[];
const myType: v.Type<T> = v.lazy(() => v.union(v.string(), v.array(myType)));
自定义验证器
这个 .assert() 方法可用于自定义验证逻辑,比如检查对象属性在内部的一致性.
const Span = v
.object({ start: v.number(), end: v.number() })
.assert((obj) => obj.start <= obj.end);
Span.parse({ start: 1, end: 2 });
// { start: 1, end: 2 }
Span.parse({ start: 2, end: 1 });
// ValitaError: custom_error at . (validation failed)
你也可以这样 细化 输入类型通过 a 类型上游。注意类型上游必须具有相容的输入类型.
function isEventHandlerName(s: string): s is `on${string}` {
return s.startsWith("on");
}
const e = v.string().assert(isEventHandlerName);
const name: `on${string}` = e.parse("onscroll");
// "onscroll"
e.parse("Steven");
// ValitaError: custom_error at . (validation failed)
每个 .assert(...) 返回新的验证符,这样您就可以进一步完善已经完善过的类别。 您也可以通过自定义的失败消息.
const Integer = v.number().assert((n) => Number.isInteger(n), "not an integer");
const Byte = Integer.assert((i) => i >= 0 && i <= 255, "not between 0 and 255");
Byte.parse(1);
// 1
Byte.parse(1.5);
// ValitaError: custom_error at . (not an integer)
Byte.parse(300);
// ValitaError: custom_error at . (not between 0 and 255)
自定义验证器可以像其他内置验证器一样使用. 这意味着您可以定义适合特定用途的助手,并反复重复使用.
// Reusable custom validator
const Organization = v
.object({
name: v.string(),
active: v.boolean(),
})
.assert((org) => org.active);
// Reuse the custom validator
const Transaction = v.object({
buyer: Organization,
seller: Organization,
amount: v.number(),
});
自定义解析器
虽然 .assert(...) 可以确保一个值是有效的,甚至可以完善该值的类型,不能改变该值本身. 然而,有时我们可能想要验证和改变这一价值.
这个 .map(...) 当知道转换不能失败时,方法对案例是很好的。 输出类型不必保持不变:
const l = v.string().map((s) => s.length);
l.parse("Hello, World!");
// 13
l.parse(1);
// ValitaError: invalid_type at . (expected string)
这个 .chain(...) 方法更强:它也可以用于解析可能失败的情况. 设想一个 JSON API 输出日期为 YYYY- MM- DD 格式, 我们希望返回一个有效的 Date 从我们的验证阶段:
{
"created_at": "2022-01-01"
}
.chain(...), 很像地图, 得到一个函数, 它会传递原始值作为第一个参数 。 如果转换失败, 我们会返回一个错误( 带有可选信件) v.err(...)如果不是,则返回转换后的值为: v.ok(...)。 。 。 .
const DateType = v.string().chain((s) => {
const date = new Date(s);
if (isNaN(+date)) {
return v.err("invalid date");
}
return v.ok(date);
});
const APIResponse = v.object({
created_at: DateType,
});
APIResponse.parse({ created_at: "2022-01-01" });
// { created_at: 2022-01-01T00:00:00.000Z }
APIResponse.parse({ created_at: "YOLO" });
// ValitaError: custom_error at .created_at (invalid date)
对于两者 .map(...) 财务报告和审定财务报表 .chain(...) 我们强烈建议你避免改变输入值。 倒不如退回一个新的值.
v.object({ name: v.string() }).map((obj) => {
// Mutating the input value like below is highly discouraged:
// obj.id = randomUUID();
// Return a new value instead:
return { ...obj, id: randomUUID() };
});
转换验证器
这个 .as(...) 方法允许您对验证器应用可重复使用的帮助。 它立即用当前验证符调用给定的函数作为第一个参数,并返回函数的结果.
例如,使验证器输出类型只读的帮助器可以这样定义和使用:
function readonly<T>(t: v.Type<T>): v.Type<Readonly<T>> {
return t;
}
const User = v.object({ id: v.string() }).as(readonly);
type User = v.Infer<typeof User>;
// type User = { readonly id: string; }
帮助者也可以通过返回一个函数来进行配置,比如在验证器输出类型中添加一个品牌的下列帮助者:
declare class Branded<B extends string | symbol> {
#brand: { [K in B]: true };
}
function branded<B extends string | symbol>(_b: B) {
return <T>(t: Type<T>): Type<T & Branded<B>> => t as Type<T & Branded<B>>;
}
const UserId = v.string().as(branded("UserId"));
const userId = UserId.parse("user-123");
// "user-123"
type UserId = typeof userId;
// type UserId = string & Branded<"UserId">
解析而不丢弃
这个 .parse(...) 当验证或解析失败时,迄今为止使用的方法会丢出一个 ValitaError 。 这个 .try(...) 方法可以用于选择只抛出实际例外的情况,例如编码错误. 解析模式也被支持.
const o = v.object({ name: v.string() });
o.try({ name: "Acme Inc." });
// { ok: true, value: { name: "Acme Inc." } }
o.try({ name: "Acme Inc.", country: "Freedomland" }, { mode: "strip" });
// { ok: true, value: { name: "Acme Inc." } }
o.try({});
// { ok: false, message: "missing_value at .name (missing value)" }
这个 .ok 属性可以用来以安全的方式检查结果.
// Fail about 50% of the time
const r = o.try(Math.random() < 0.5 ? { name: "Acme Inc." } : {});
if (r.ok) {
// r.value is defined within this block
console.log(`Success: ${r.value}`);
} else {
// r.message is defined within this block
console.log(`Failure: ${r.message}`);
}
允许进一步组成, .try(...)返回值与 .chain(...)。链式函数还收到第二个包含解析模式的参数,并可以转发给 .try(...)。 。 。 .
const Company = v.object({ name: v.string() });
const CompanyString = v.string().chain((json, options) => {
let value: unknown;
try {
value = JSON.parse(json);
} catch {
return v.err("not valid JSON");
}
return Company.try(value, options);
});
CompanyString.parse('{ "name": "Acme Inc." }');
// { name: "Acme Inc." }
CompanyString.parse('{ "name": "Acme Inc.", "ceo": "Wiley E. Coyote" }');
// ValitaError: unrecognized_keys at . (unrecognized key "ceo")
// The parsing mode is forwarded to .try(...)
CompanyString.parse('{ "name": "Acme Inc.", "ceo": "Wiley E. Coyote" }', {
mode: "strip",
});
// { name: 'Acme Inc.' }
原来这样编译的解析器比较常见. 因此,你可以传递类型到 .chain(...)以下内容与上述定义相同: CompanyString数字 :
const Company = v.object({ name: v.string() });
// We now have a handy common helper for parsing JSON strings!
const JsonString = v.string().chain((json) => {
try {
return v.ok(JSON.parse(json));
} catch {
return v.err("not valid JSON");
}
});
const CompanyString = JsonString.chain(Company);
推断输出类型
校验器的确切输出类型可以是 推断 从类型校验器使用的 v.Infer<typeof ...>数字 :
const Person = v.object({
name: v.string(),
age: v.number().optional(),
});
type Person = v.Infer<typeof Person>;
// type Person = { name: string, age?: number };
类型构成提示( T)
减少、再使用、再利用
这个库的 API 接口被故意保留小—— 对于一些小的定义. 因此,我们鼓励管理一个适合你具体需要的帮助者图书馆。 例如,可重复使用的帮助工具,以确保数字在某一特定范围之间被定义和使用:
function between(min: number, max: number) {
return (n: number) => {
if (n < min || n > max) {
return v.err("outside range");
}
return v.ok(n);
};
}
const num = v.number().chain(between(0, 255));
已解构的助手
一些验证器类型提供了额外的属性和方法来进行回顾和进一步转化. 其中一个案例是 v.object(...)是因为 .shape 包含每个财产验证符的属性.
const Company = v.object({
name: v.string().assert((s) => s.length > 0, "empty name"),
});
Company.shape.name.parse("Acme Inc.");
// "Acme Inc."
但是,因为 .assert(...), (中文(简体) ), .map(...) 财务报告和审定财务报表 .chain(...) 可能都几乎任意限制和转换输出类型,其返回的验证器可能没有原输出类型特有的属性或方法。 例如,经过改进的 v.object(...) 校验符不会有 .shape 属性。 因此,下列情况将行不通:
const Company = v
.object({
name: v.string().assert((s) => s.length > 0, "empty name"),
employees: v.number(),
})
.assert((c) => c.employees >= 0);
const Organization = v.object({
// Try to reuse Company's handy name validator
name: Company.shape.name,
});
// TypeScript error: Property 'shape' does not exist on type 'Type<{ name: string; }>'
建议的解决方案是将原始验证器解构得足够多,使常见的棋子能够被直接再用:
const NonEmptyString = v.string().assert((s) => s.length > 0, "empty");
const Company = v
.object({
name: NonEmptyString,
employees: v.number(),
})
.assert((c) => c.employees >= 0);
const Organization = v.object({
name: NonEmptyString,
});
许可证
这个图书馆根据麻省理工学院的许可证颁发执照. 见 自由主义。 。 。 .