npm package: @seayoo-web/validator
Description: javascript data validator
Author: web@seayoo.com
Latest version: 2.3.0

# Validator 数据类型校验器

## 基本用法1：用已有类型约束验证器编写

```ts
// 导入定义工具
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：直接定义验证器并推导类型

```js
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"; }
```

## 功能简介

1. 通过 ts 类型定义约束 validator 的编写
2. 通过无约束的 validator 自动推导 ts 类型
3. 支持 Fastify 的 schema 验证（详见下方描述）

通过上述功能，使得

1. 运行时的类型检查和 ts 定义尽可能保持严格一致，降低类型错误导致的潜在问题
2. 减少中间临时类型定义和守卫函数编写，保持代码精简高效

## 基本概念

### 类型校验器 validator

校验器是一个 `class`，提供条件输入和 `validate` 功能来检查一个传入的数据是否符合特性类型定义要求。`validate` 函数返回了一个[类型谓词](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates)。

### 类型守卫 guard

类型守卫是一个 ts 概念，通过类型守卫检查的数据符合类型谓词约束，达到了[类型收窄](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#handbook-content)的目的。我们这里特指的是 `类型守卫函数` 其定义为 `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` ：含有 `string` `number` `symbol` 至少一个类型作为索引且元素类型一致的 `plain object`；
- `object`：不是 `record` 且具有有限数量索引的 `object`，其元素类型没有一致性约束；

### 联合对象 union 和枚举 enum

[联合类型](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html#unions)是特殊的类型组合，而[枚举](https://www.typescriptlang.org/docs/handbook/enums.html#handbook-content)是一组特殊字面量的组合且[已经不被推荐使用](https://www.typescriptlang.org/docs/handbook/enums.html#const-enum-pitfalls)。

在本工具中，没有对 `enum` 的直接支持，在 `StringValidator` `NumberValidator` 和 `BigintValidator` 中提供 `enum` 方法来检查数据的可选范围实现类似的约束。而 `union` 定义跟 ts 定义相同，只不过由于 `null` 和 `undefined` 过于常用，这两个类型与其他类型的联合由对应的校验器实现：即所有 `validator` 都支持 `optional` 和 `maybeNull` 两个方法以支持和 `undefined` `null` 的联合。

具体来说：

- 如果需要使用枚举，可以使用 `const enum` 和 `object as const` 来替代，[点击这里查看](https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums)。在本工具中按照实际枚举的类型来设定校验器；
- `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 的设置：

```ts
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); // false
```

### lock()

同上原因，验证器是引用型数据，当不希望验证器被意外修改时，可以锁定验证器，锁定后的验证器将不再接受任何验证条件的变更：

```ts
const s1 = v.number().enum(1, 2).lock();
s1.validate(1); // true

type S1Type = InferType<typeof s1>; // 锁定后仅仅剩余部分方法可用 { clone, validate, check, convert }
```

## StringValidator

字符串校验器，用法示例：

```ts
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。示例如下：

```ts
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 类型校验器，示例如下：

```ts
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

布尔类型校验器，示例如下：

```ts
v.bool();
v.bool().optional().maybeNull();
```

## ObjectValidator

对象类型校验器，示例如下：

```ts
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()
```

对象验证器是使用频次较高的一个类型，为了获取对应的类型守卫函数，有四个方法，这些方法都支持泛型参数以指定要校验的类型，然后用来约束验证器的编写：

```ts
// 方法一
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` 来定义公共对象的校验器：

```ts
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;
```

### 空对象检测

```ts
// 导入内置的 EmptyObject / EmptyObjectValidator 工具

import { type EmptyObject, EmptyObjectValidator } from "@seayoo-web/validator";

// 另有空对象检测方法，见 RecordValidator
```

## RecordValidator

Record 类型校验器，默认输出类型为 string Record，即 Record<string, T>

```ts
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 的类型守卫，以下是推荐用法：

```ts
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 的类型。

:::

### 空对象检测

```ts
const emptyRecordValidator = v.record(v.never()); // 对应的数据类型为 Record<string, never>

// 另有内置的 EmptyObject 和 EmptyObjectValidator 工具
```

## ArrayValidator

数组类型校验器，示例如下

```ts
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 的类型守卫，以下是推荐用法

```ts
import { typedArrayGuard } from "@seayoo-web/utils";

const guard1 = v.guard<number[]>(v.array(v.number()));
const guard2 = typedArrayGuard<number>(v.number());
```

## TupleValidator

元组类型校验器，示例如下

```ts
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); // false
```

## UnionValidator

联合类型校验器，其中 `null` `undefined` 的处理由各个验证器独立实现，UnionValidator 主要负责对其他类型联合的处理。示例如下

```ts
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 的编写：

```ts
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 允许比定义类型更窄的值进行赋值：

```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` 方法，通过泛型参数进一步反向（假设从类型推导验证器为正向）进行类型约束：

```ts
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

自定义类型校验器，用于上述校验器无法覆盖的场景，通过自定义类型守卫来实现验证，示例如下：

```ts
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`

```ts
const s = v.unknown();
s.validate(""); // validate 验证任何数据结果都是 true
```

## NeverValidator

校验一个 `never` 类型数据，实际上 `validate` 没有任何逻辑，直接返回 `false`

```ts
const s = v.never();
s.validate(""); // validate 验证任何数据结果都是 false
```

## 自定义格式化函数

默认情况下，校验错误信息输出为英文，可以通过 `setDefaultErrorFormatter` 设置新的格式化函数：

```ts
import { setDefaultErrorFormatter } from "@seayoo-web/validator";

setDefaultErrorFormatter(function (error, ignorePath) {
  let message: string = "";
  switch (error.type) {
    case ValidateErrorType.TypeNotMatch:
      message = error.actual === "undefined" ? `字段不能为空` : `期望是 ${error.expected}，实际是 ${error.actual}`;
      break;
    case ValidateErrorType.OutOfRange:
      message = `${error.value} 不在区间范围 [${error.min}, ${error.max}]`;
      break;
    case ValidateErrorType.ValueNotAllowed:
      message = error.allowlist
        ? `${error.value} 不被允许，仅支持 ${error.allowlist.join(", ")}`
        : `${error.value} 不被允许`;
      break;
    case ValidateErrorType.PatternNotMatch:
      message = `${error.value} 不符合格式要求 ${error.pattern}`;
      break;
    case ValidateErrorType.LengthNotMatch:
      message = `长度 ${error.actual} 不在区间范围 [${error.min}, ${error.max}]`;
      break;
    case ValidateErrorType.KeyConfigError:
      message =
        error.value === undefined
          ? `key 字段 "${error.key}" 没有找到`
          : `key 字段 "${error.key}"（${error.value}）不满足联合类型定义`;
      break;
    case ValidateErrorType.CustomValidationError:
      message = `自定义验证器（${error.validation}）校验失败`;
      break;
    default:
      message = `未知的校验错误 ${JSON.stringify(error)}`;
      break;
  }
  return ignorePath ? message : `${error.path || ""} ${message}`;
});
```

## Fastify 集成

可以使用本工具把 `IDataValidator` 与 Fastify 的类型系统进行集成，达到在编译期和运行时都能受益的效果。

主要导出（可直接从包中导入）：

- `FastifyValidatorCompiler`：用于 `app.setValidatorCompiler(...)`，将 `IDataValidator` 转为 Fastify 可识别的校验函数。
- `FastifySerializerCompiler`：用于 `app.setSerializerCompiler(...)`，将 `IDataValidator` 转为 Fastify 可识别的校验函数。
- `FastifyTypeProvider`：TypeScript 类型提供器，配合 `app.withTypeProvider<FastifyTypeProvider>()` 使用以获得类型推导。
- `FastifyPluginCallback` / `FastifyPluginAsync`：按本库类型推导的插件签名。
- `FastifyInstance`：带本类型校验器的 `FastifyInstance` 类型别名。

简化示例：

```ts
import Fastify from "fastify";
import { v, FastifyValidatorCompiler, FastifySerializerCompiler } from "@seayoo-web/validator";
import type { FastifyTypeProvider, FastifyInstance } from "@seayoo-web/validator";

// 告诉 Fastify 使用本工具进行校验/序列化/提供类型推导
const app = Fastify()
  .setValidatorCompiler(FastifyValidatorCompiler)
  .setSerializerCompiler(FastifySerializerCompiler)
  .withTypeProvider<FastifyTypeProvider>();

// 此时app的类型已经包含了工具 FastifyTypeProvider
app.get(
  "/test",
  {
    schema: {
      querystring: v.object({
        name: v.string().optional(),
      }),
      response: {
        200: v.object({
          res: v.string(),
        }),
      },
    },
  },
  function (req, reply) {
    // 此处 req.query 以及 return 值的类型会由自定义的推导工具推断
    return { res: req.query.name || "" };
  },
);

// 在 app.register 函数中，app 的类型未能正确传递内部函数的参数，需要手工指定
app.register(async function (app: FastifyInstance) {
  // 手工指定 FastifyInstance 类型后可以从 schema 中获取类型提示
  app.get(
    "/internal/test",
    {
      schema: {
        querystring: v.object({
          name: v.string().optional(),
        }),
      },
    },
    function (req, reply) {
      return { name: req.query.name };
    },
  );
});
```