@seayoo-web/validator
v2.1.2
Published
javascript data validator
Keywords
Readme
Validator 数据类型校验器
基本用法1:用已有类型约束验证器编写
// 导入定义工具
import { v } from "@seayoo-web/validator";
// 假设类型定义如下
const enum Gender {
Male: "male"
Female: "female"
}
interface User {
name: string
gender: Gender
}
// 定义类型校验器,传入泛型参数进行检查约束
const sch = v.object<User>({
name: v.string().disallow(""),
gender: v.string().enum(Gender.Male, Gender.Female)
})
// 检查数据是否符合要求
if (sch.validate("some unknown data")) {
// ...
}
// 也可以将校验器转成类型守卫函数
const isUser = v.guard(sch);
if(isUser("some unknown data")) {
// ...
}
// 如果为了获得类型守卫函数,可以使用内置的 objectGuard
import { v, objectGuard } from "@seayoo-web/validator"
const isUser = objectGuard<User>({
name: v.string().disallow(""),
gender: v.string().enum(Gender.Male, Gender.Female)
})基本用法2:直接定义验证器并推导类型
import { v, type InferType } from "@seayoo-web/validator"
const sch = v.object({
name: v.string().disallow(""),
gender: v.string().enum("male", "female")
})
type User = InferType<typeof sch>
// type User = { name: string; gender: "male" | "female"; }功能简介
- 通过 ts 类型定义约束 validator 的编写
- 通过无约束的 validator 自动推导 ts 类型
- 支持 Fastify 的 schema 验证(详见下方描述)
通过上述功能,使得
- 运行时的类型检查和 ts 定义尽可能保持严格一致,降低类型错误导致的潜在问题
- 减少中间临时类型定义和守卫函数编写,保持代码精简高效
基本概念
类型校验器 validator
校验器是一个 class,提供条件输入和 validate 功能来检查一个传入的数据是否符合特性类型定义要求。validate 函数返回了一个类型谓词。
类型守卫 guard
类型守卫是一个 ts 概念,通过类型守卫检查的数据符合类型谓词约束,达到了类型收窄的目的。我们这里特指的是 类型守卫函数 其定义为 function<T>(data: unknown) => data is T 。
数据类型
除了基本数据类型外,以下几组数据类型需要注意区别:
数组 array 和 元组 tuple
这两个数据类型本质上都是数组,区别在于 array 长度不固定且所有元素类型都一致,tuple 的长度固定,元素类型没有约束。比如所有用户数据 User[] 就是数组,而坐标点 [number, number] 就是元组。
对象 object 和记录 record
原则上 object 囊括了大多数 js 复杂类型,在多数的场景下,我们其实默认的是 plain object 即一个没有原型链或原型链为 null 的 纯对象,而其他 object 类型的数据类型都会称呼它们自己特有的名字,比如日期 Date 或者 函数 Function。
record 是一个特殊的 plain object,其 ts 签名为 type Record<K extends keyof any, V> = {[P in K]: T},即一个拥有属于 string | number | symbol 索引的对象,其元素类型全部相同。在本工具中 record 定义跟 ts 略有区别,比如 Record<"a"|"b", number> 在 ts 中是合法的 Record,但在本工具范畴中,它属于 object,更确切的表述为:
record:含有stringnumbersymbol至少一个类型作为索引且元素类型一致的plain object;object:不是record且具有有限数量索引的object,其元素类型没有一致性约束;
联合对象 union 和枚举 enum
联合类型是特殊的类型组合,而枚举是一组特殊字面量的组合且已经不被推荐使用。
在本工具中,没有对 enum 的直接支持,在 StringValidator NumberValidator 和 BigintValidator 中提供 enum 方法来检查数据的可选范围实现类似的约束。而 union 定义跟 ts 定义相同,只不过由于 null 和 undefined 过于常用,这两个类型与其他类型的联合由对应的校验器实现:即所有 validator 都支持 optional 和 maybeNull 两个方法以支持和 undefined null 的联合。
具体来说:
- 如果需要使用枚举,可以使用
const enum和object as const来替代,点击这里查看。在本工具中按照实际枚举的类型来设定校验器; union类型校验由UnionValidator来检查,常用的null和undefined由各个校验器自行检查;
校验器通用方法
所有校验器支持以下方法:
check(value: unknown): true | TypeCheckError | TypeCheckError[]
检查一个数据,返回检查结果:ture表示检查通过,其他表示有一个或多个错误
convert(value: unknown): unknown
尝试将入参的值转化为目标类型,其中联合类型校验器和自定义校验器原封返回参数,如果参数为 null / undefined 也原封返回。
validate(value: unknown): data is T
检查一个数据,返回当前验证器对应的类型断言
其他通用方法
除了 v.unknown 和 v.never 外的验证器都支持以下通用方法:
optional()
设置数据为可选,即数据支持 undefined
maybeNull()
设置数据可空,即数据支持 null
clone()
验证器是对象实例,属于引用型数据,任何调用都会修改原数据,为了获取一个配置相同的验证器,可以调用 clone 方法,其中 clone 方法不会复制 optional / maybeNull 的设置:
const s1 = v.object({ name: v.string() });
s1.validate(null); // false
const s2 = s1.maybeNull();
s2.validate(null); // true
s1.validate(null); // true ⚠️ 这可能不是预期的行为!!
// 可以使用 clone 方法来获取一个新校验器,并忽略 optional / maybeNull 的配置
// 原来的校验器过滤条件状态不受影响
const s3 = v1.clone();
s3.validate(null); // false
s3.validate(undefine); // falselock()
同上原因,验证器是引用型数据,当不希望验证器被意外修改时,可以锁定验证器,锁定后的验证器将不再接受任何验证条件的变更:
const s1 = v.number().enum(1, 2).lock();
s1.validate(1); // true
type S1Type = InferType<typeof s1>; // 锁定后仅仅剩余部分方法可用 { clone, validate, check, convert }StringValidator
字符串校验器,用法示例:
const s1 = v.string(); // string
const s2 = v.string().optional(); // string | undefined
const s3 = v.string().maybeNull(); // string | null
// 所有校验器都提供 validate 方法来验证传入的数据是否符合要求
s1.validate("aa"); // true
s1.validate(undefined); // false
s2.validate(undefined); // true
s3.validate(undefined); // false
// 后续校验器的 optional / maybeNull 都是相同的,且可以和过滤条件方法连写,比如
v.string()
.url()
.optional()
.disallow("http://127.0.0.1", /^http:\/\/localhost(?:\d+)?/i);
// 字符串格式校验
v.string().url().dataUri(); // 允许 url 或 dataURI 格式的字符串
v.string().iosTime(); // 允许 ios 时间字符串,比如 2025-03-28T00:38:31.516Z
v.string().disallow("", /^\s+$/); // 不允许空字符串和空格字符串,支持正则表达式
v.string().pattern(/^[a-z][a-z\d]{3,}$/); // 自定义字符串验证正则
v.string().enum("A", "B", "C"); // 自定义可选字符串,设置后其他过滤条件将失效
// 如果需要校验模板字符串类型,可以这么做
const t1 = v.string().pattern<`${number}`>("NumberString", /^\d+$/);
const t2 = v.string().pattern<`${string}@seayoo.com`>("SeayooAccount", /.+@seayoo\.com$/);NumberValidator
数字类型校验器,默认情况下内置了条件:在 [-2^53+1, 2^53-1] 且不允许 Nan 和 Infinity。示例如下:
v.number(); // 在安全整数内,且不允许 NaN 和 Infinity
v.number().optional().maybeNull(); // 可选且可为 null
v.number().min(1).max(10).disallow(1); // 在 (1, 10] 范围内的数字
v.number().min(0).integer(); // 大于等于 0 的安全整数
v.number().allowNaN().allowInfinity(); // 允许任意 number
v.number().unsafe(); // 允许超过安全整数范围外的数字,比如一个超大的浮点数,但不允许 Infinity
v.number().enum(1, 2, 3); // 仅仅允许 1 2 3,设置后其他过滤条件将失效BigIntValidator
BigInt 类型校验器,示例如下:
v.bigint().optional().maybeNull(); // BigInt | undefined | null
v.bigint().min(1n).max(10n).disallow(2n); // [1, 2) (2, 10]
v.bigint().enum(2n, 4n, 6n); // 仅允许 2 4 6,设置后其他过滤条件将失效BooleanValidator
布尔类型校验器,示例如下:
v.bool();
v.bool().optional().maybeNull();ObjectValidator
对象类型校验器,示例如下:
import { type InferType, v } from "@seayoo-web/validator"
const o1 = v.object({
name: v.string(),
id: v.number().min(1).integer(),
address: v.object({
city: v.string(),
code: v.string(),
}).optional()
}).maybeNull()
// 上述校验器对应的类型为
// null | { name: string, id: number, address?: { city: string, code: string } }
// InferType 工具用于读取验证器守卫数据的类型
type UserInfoWithNull = InferType<typeof o1>
// 可以手工剔净
type UserInfo = NonNullable<UserInfoWithNull>
o1.validate({ name: "Jack", id: 1 }) // true
o1.validate({ name: "Mark", id: 0 }) // false
// 如果对已有类型关联校验器,可以传入泛型参数
// 假设类型定义如下
const enum Gender {
Male: "male"
Female: "female"
}
interface User {
name: string
gender: Gender
}
// 设定泛型参数后可以自动检查验证器编写是否正确
const o2 = v.object<User>({
name: v.string().disallow(""),
gender: v.string().enum(Gender.Male, Gender.Female)
})
o2.validate({ name: "", gender: Gender.Male }) // false
o2.validate({ name: "Jack", gender: "unknown" }) // false
// 如果需要约束数据是 plain object
v.object({ ... }).plain()
// 如果需要检查某一个字段是否匹配
const m = v.object({ type: v.string().enum("A", "B") })
m.match("type", "A") // true
m.match("type", "C") // false
// 当需要复用验证器定义,有两个方法
// 方法1,单独保存 objectShape, v.shape 是用于定义 ObjectValidator 参数的辅助工具
const objShape = v.shape<DataType>({ ... })
const o3 = v.object(objShape).optional()
const o4 = v.object(objShape).maybeNull()
// 方法2,使用 clone 方法,💡clone 方法不会复制 optional / maybeNull 的配置
const o5 = v.object<DataType>({ ... }).optional()
const o6 = o5.clone().maybeNull()对象验证器是使用频次较高的一个类型,为了获取对应的类型守卫函数,有四个方法,这些方法都支持泛型参数以指定要校验的类型,然后用来约束验证器的编写:
// 方法一
const o1 = v.object({ ... }) // or: v.object<T>({ ... })
const guard1 = o1.validate.bind(o1)
// 方法二,推荐
const guard2 = v.guard(v.object({ ... })) // or: v.guard<T>(v.object({ ... }))
// 方法三,推荐,此方法会强制约束为 plain object
const guard3 = typedObjectGuard({ ... }) // or: typedObjectGuard<T>({ ... })
// 方法四
const o4 = v.object({ ... })
const guard4 = function(data: unknown): data is InferType<typeof o4>{
return o4.validate(data)
}在定义多个相似的对象时,可以通过 v.shape 来定义公共对象的校验器:
const userBaseInfoShape = v.shape<UserBaseInfo>({
name: v.string(),
gender: v.number().enum(1, 2),
});
const availableUser = v.object<UserBaseInfo & { status: "available"; score: number }>({
...userBaseInfoShape,
status: v.string().enum("available"),
score: v.number(),
});
const removedUser = v.object<UserBaseInfo & { status: "removed"; removeAt: string }>({
...userBaseInfoShape,
status: v.string().enum("removed"),
removeAt: v.string().iosTime(),
});
// 也可以通过 clone 方法来复用定义配置
const userMaybeNull = availableUser.clone().maybeNull();
// 也可以从定义好的验证器中提取 shape(浅层副本)
const userShape = availableUser.shape;空对象检测
// 导入内置的 EmptyObject / EmptyObjectValidator 工具
import { type EmptyObject, EmptyObjectValidator } from "@seayoo-web/validator";
// 另有空对象检测方法,见 RecordValidatorRecordValidator
Record 类型校验器,默认输出类型为 string Record,即 Record<string, T>
const r1 = v.record(v.number()); // 对应数据类型为 Record<string, number>
r1.validate({ a: 1 }); // true
r1.validate({ a: 1, b: "2" }); // false
// 支持 optional / maybeNull
const r2 = v.record(v.number()).maybeNull();
r2.validate(null); // true
// 可以设置元素类型来约束验证器
const r3 = v.record<number>(v.number());
// 也可以使用辅助工具 v.define 来定义,注意泛型参数不一样
// 此时可以指定 Record 的 key 类型 💡运行时不检查任何 key 的类型
const r4 = v.define<Record<string, number>>(v.record(v.number));
// 支持 clone 方法复制配置 💡clone 方法不会复制 optional / maybeNull 的配置
const r5 = r2.clone();
r5.validate(null); // false同 ObjectValidator 相似,可以有多种方法来获取 record 的类型守卫,以下是推荐用法:
import { typedRecordGuard } from "@seayoo-web/validator";
const guard1 = v.guard<Record<string, number>>(v.record(v.number()));
const guard2 = typedRecordGuard<number>(v.number());:::alert
注意,RecordValidator 运行时并不对 record key 做任何检查。record key 的类型仅仅在 ts 状态中生效,可以通过 v.define v.guard 来设定 record key 的类型。
:::
空对象检测
const emptyRecordValidator = v.record(v.never()); // 对应的数据类型为 Record<string, never>
// 另有内置的 EmptyObject 和 EmptyObjectValidator 工具ArrayValidator
数组类型校验器,示例如下
const ar1 = v.array(v.number()) // 对应数据类型为 number[]
ar1.validate([]) // true
ar1.validate([1, 2, "3"]) // false
// 支持 optional / maybeNull
const ar2 = v.array(v.number()).maybeNull()
ar2.validate(null) // true
// 可以限制数组的长度
v.array(...).min(1).max(100) // 最少有一个元素,最多不超过100个元素
// 也可以 min == max
v.array(...).min(2).max(2) // 即约束数组的长度必须是 2
// 可以通过设置元素类型来约束验证器编写
const ar3 = v.array<number>(v.number())
// 也可以通过 v.define 来约束,注意泛型类型
const ar4 = v.define<number[]>(v.array(v.number()))
// 支持 clone 方法复制配置 💡clone 方法不会复制 optional / maybeNull 的配置
const ar5 = ar2.clone()
ar2.validate(null) // false同 ObjectValidator 相似,可以有多种方法来获取 array 的类型守卫,以下是推荐用法
import { typedArrayGuard } from "@seayoo-web/utils";
const guard1 = v.guard<number[]>(v.array(v.number()));
const guard2 = typedArrayGuard<number>(v.number());TupleValidator
元组类型校验器,示例如下
const t1 = v.tuple(v.number(), v.string()); // 对应数据类型为 [number, string]
t1.validate([1, "2"]); // true
t1.validate([1, "2", 3]); // false
t1.validate(["1", "2"]); // false
// 支持 optional / maybeNull
const t2 = v.tuple(v.number(), v.string()).maybeNull();
t2.validate(null); // true
// 💡与 object 不同,tuple 类型的泛型参数不是数据类型本身,而是校验器 validator
import { type InferValidator } from "@seayoo-web/utils";
const t3 = v.tuple<[InferValidator<number>, InferValidator<string>]>(v.number(), v.string());
// 可以通过 v.define 来简化定义
const t4 = v.define<[number, string]>(v.tuple(v.number(), v.string()));
// 可以通过 v.guard 获取类型守卫
const guard = v.guard<[number, string]>(v.tuple(v.number(), v.string()));
// 支持 clone 方法复制配置 💡clone 方法不会复制 optional / maybeNull 的配置
const t5 = t2.clone();
t5.validate(null); // falseUnionValidator
联合类型校验器,其中 null undefined 的处理由各个验证器独立实现,UnionValidator 主要负责对其他类型联合的处理。示例如下
const student = v.object({ name: v.string(), classRoom: v.number() });
const teacher = v.object({ name: v.string(), subject: v.string() });
// 对应数据类型为 {name: string, classRoom: number} | {name: string, subject: string}
const schoolMember = v.union(student, teacher);
// 支持 optional / maybeNull
const u1 = v.union(v.number(), v.string()).maybeNull();
u1.validate(null); // true
//💡与 object 不同,union 类型的泛型参数不是数据类型本身,而是校验器 validator
// 可以通过 v.define 进行类型约束
const u2 = v.define<number | string>(v.union(v.number(), v.string()));
// 可以通过 v.guard 获取类型守卫
const guard = v.guard<string | number>(v.union(v.number(), v.string()));
// 支持 clone 方法复制配置 💡clone 方法不会复制 optional / maybeNull 的配置
const u3 = u1.clone();
u3.validate(null); // false由于 ts 的推导限制,从 union 到 tuple 的推导顺序是不固定的,就导致无法直接从 union 类型来约束 validator 的编写:
const s = v.define<string | number | bigint>(v.union(v.string(), v.number(), v.bigint()));
// 如果从 string | number | bigint 推导出 [StringValidator, NumberValidator, BigIntValidator] 的话上述代码就是类型检测通过的,因为 v.union 方法的参数顺序刚好跟推导出的类型一致
// 不幸的是,从 string | number | bigint 可能推导出 [BigIntValidator, StringValidator, NumberValidator] 这时 v.union 的参数顺序就跟推导出的类型不一致了,哪怕运行时逻辑是正确的为了解决这一问题,UnionValidator 不再尝试从 union 推导 tuple,而是将 union 作为整体返回。这又引发另外一个问题:ts 允许比定义类型更窄的值进行赋值:
const s = v.object<{ name: string | number | bigint }>({
name: v.union(v.string(), v.number()),
});
// 上述代码中 name 被推导为 UnionValidator<string | number | bigint>,其赋值的类型为 UnionValidator<string | number>
// 上述代码在 ts 的赋值逻辑中完全是正确的,这显然不符合我们对验证器的定义:不能多,不能少也不能错为了解决上述问题,UnionValidator 引入 satisfies 方法,通过泛型参数进一步反向(假设从类型推导验证器为正向)进行类型约束:
type NameType = string | number | bigint;
const s = v.object<{ name: NameType }>({
name: v.union(v.string(), v.number()).satisfies<NameType>(), // 设置后将检测 union 类型,不符合则进行错误提示
});添加 satisfies 并提供设置泛型后,上述代码将被提示错误,在补足 v.bigint 后错误消失。
CustomValidator
自定义类型校验器,用于上述校验器无法覆盖的场景,通过自定义类型守卫来实现验证,示例如下:
function isSeayooHost(data: unknown): data is `${string}.seayoo.com` {
return typeof data === "string" && data.endsWith(".seayoo.com");
}
const c = v.custom("SeayooHost", isSeayooHost);
c.validate("www.seayoo.com"); // true自定义类型可以嵌入到 ObjectValidator / RecordValidator / ArrayValidator / TupleValidator / UnionValidator 中用以实现各种复杂类型的处理和校验。这是一个兜底的方案,大多数场景应该用不到它。
UnknownValidator
校验一个 unknown 类型数据,实际上 validate 没有任何逻辑,直接返回 true
const s = v.unknown();
s.validate(""); // validate 验证任何数据结果都是 trueNeverValidator
校验一个 never 类型数据,实际上 validate 没有任何逻辑,直接返回 false
const s = v.never();
s.validate(""); // validate 验证任何数据结果都是 falseFastify 集成
可以使用本工具把 IDataValidator 与 Fastify 的类型系统进行集成,达到在编译期和运行时都能受益的效果。
主要导出(可直接从包中导入):
FastifyValidatorCompiler:用于app.setValidatorCompiler(...),将IDataValidator转为 Fastify 可识别的校验函数。TypeProviderOfFastify:TypeScript 类型提供器,配合app.withTypeProvider<TypeProviderOfFastify>()使用以获得类型推导。FastifyPluginCallback/FastifyPluginAsync:按本库类型推导的插件签名。FastifyInstance:带本类型校验器的FastifyInstance类型别名。
简化示例:
import Fastify from "fastify";
import { v, FastifyValidatorCompiler } from "@seayoo-web/validator";
import type { TypeProviderOfFastify } from "@seayoo-web/validator";
const app = Fastify().setValidatorCompiler(FastifyValidatorCompiler).withTypeProvider<TypeProviderOfFastify>();
app.get(
"/test",
{
schema: {
querystring: v.object({
name: v.string().optional(),
}),
},
},
function (req, reply) {
// 此处 req.query 的类型会由 TypeProviderOfFastify 推断
return { name: req.query.name };
},
);