validkit 是一个代码优先(code-first)的模式声明与运行时数据校验工具库:用一套链式 API 描述数据形状,它同时帮你完成运行时校验、类型推断、输入纠偏与错误信息国际化。
- 🫠 类型只在编译期存在:TypeScript 的
interface编译后消失,来自接口、表单、URL 参数、本地存储的数据依然可能是任意形状。 - 🌍 错误信息要重复造轮子:主流校验库默认只有英文,面向中文用户的产品需要手写 errorMap,繁琐且容易遗漏。
- 🧾 中文场景格式校验缺失:手机号之外,二代身份证、统一社会信用代码等带国家标准校验位的格式很少有库内建。
- 🧳 依赖体积焦虑:表单、BFF、SDK 都要校验,没人想为它引入一棵庞大的依赖树。
| 亮点 | 说明 |
|---|---|
| 🌐 内建双语错误信息 | 英文 / 简体中文开箱即用,支持全局切换、单次解析切换、按错误码自定义模板,无需手写 errorMap |
| 🧾 中文格式全家桶 | 大陆手机号、二代身份证(GB 11643-1999 MOD11-2 校验位)、统一社会信用代码(GB 32100-2015 MOD31 校验位)、邮编、QQ 号 |
| 🪶 真·零运行时依赖 | dependencies 为空,ESM + CJS 双产物 + .d.ts 类型声明,Node ≥16 与现代浏览器通用,可 Tree-shaking |
| 🔌 四种消费姿势 | parse(抛异常)/ safeParse(结果对象)/ isValid(类型守卫)/ assert(断言函数),另支持异步 refineAsync |
| 📤 JSON Schema 导出 | 一行 v.toJsonSchema() 导出 Draft-07,便于对接 OpenAPI、表单渲染器等生态 |
| 🖥️ CI 友好 CLI | validkit fmt email a@b.com 退出码即结果,Shell 脚本与流水线可直接使用 |
💡 灵感来源:本项目的产品逻辑参考了 GitHub Trending 上流行的 TypeScript 校验库 zod(MIT),但全部代码独立自研实现,并在错误信息国际化、中文格式校验、双包工程化等方向做了差异化设计,未复制其任何源码。
- ✅ 完整的标量类型:
string / number / boolean / bigint / date / literal / enum / nativeEnum / null / undefined / any / unknown / never - 🧱 丰富的复合类型:
object / array / tuple / union / intersection / record / map / set,对象支持strict / passthrough / strip / partial / pick / omit / extend - ⛓️ 链式组合能力:
optional / nullable / nullish / default / refine / refineAsync / transform / preprocess / custom / instanceOf - 🎯 精确的错误定位:嵌套对象、数组下标自动形成
user.addresses[0].city形式的错误路径,默认收集全部错误,也可abortEarly快速失败 - 🧮 Unicode 感知长度:字符串长度按码点(code point)计算,
"你好".length === 2,不会把 Emoji/中文算错 - 🔄 输入纠偏(coercion):表单字符串一键转
number / boolean / date / bigint - 📐 静态类型推断:
Infer<typeof schema>自动推导输出类型,default/transform也能正确反映到类型上 - 🧪 82 项单元测试全部通过,覆盖正常路径、边界条件、国家标准校验位与 CLI
- Node.js ≥ 16.0.0(CLI 与 CJS/ESM 引入);浏览器端可通过任意打包器(Vite / webpack / esbuild)使用
- TypeScript 项目建议 TS ≥ 5.0(纯 JS 项目无需 TypeScript)
- 运行时第三方依赖:0 个
npm install validkit
# 或
pnpm add validkit
yarn add validkitimport { v, type Infer } from 'validkit';
// 1️⃣ 声明模式
const User = v.object({
name: v.string().min(2, '姓名至少 2 个字符'),
age: v.number().int().min(0).max(150),
email: v.string().email(),
phone: v.string().cnMobile().optional(),
role: v.enum(['admin', 'member'] as const).default('member'),
});
// 2️⃣ 推导 TS 类型,无需重复维护 interface
type User = Infer<typeof User>;
// 3️⃣ 校验
const result = User.safeParse({ name: '小琳', age: 28, email: 'lin@example.com' });
if (result.success) {
console.log(result.data); // { name: '小琳', age: 28, email: ..., role: 'member' } ← default 自动补全
} else {
console.log(result.issues); // 结构化错误数组
}npx validkit fmt email ada@example.com # PASS,退出码 0
npx validkit fmt cnMobile 13800138000 # PASS
npx validkit fmt ipv4 127.0.0.1 10.0.0.1 bad # 2 PASS / 1 FAIL,退出码 1
npx validkit formats # 列出全部内建格式const Age = v.number().int().min(0);
Age.parse(20); // ✅ 返回 20
Age.parse('x'); // ❌ 抛出 ValidationError
Age.safeParse('x'); // { success: false, issues: [...], error }
Age.isValid(20); // true(类型守卫:number)
Age.assert(someUnknown); // asserts 类型守卫,失败抛错v.string()
.trim() // 先去空白
.toLowerCase() // 再转小写
.min(3).max(20) // 长度(按 Unicode 码点)
.pattern(/^[a-z0-9_]+$/) // 正则
.startsWith('id_');
// 内建格式(部分)
v.string().email();
v.string().url();
v.string().uuid();
v.string().ipv4();
v.string().ipv6();
v.string().datetime(); // ISO 8601
v.string().creditCard(); // Luhn 校验,自动忽略空格/连字符
v.string().cnMobile(); // 中国大陆手机号
v.string().cnIdCard(); // 二代身份证 + GB11643 校验位 + 出生日期合法性
v.string().cnCreditCode(); // 统一社会信用代码 + GB32100 校验位
v.string().cnPostal(); // 邮政编码
v.string().qq();
v.string().hexColor();
v.string().macAddress();
v.string().numeric();
v.string().alphanum();每条规则都支持传入自定义消息作为最后一个参数,例如
v.string().min(6, '密码至少 6 位')。
v.number().int().positive().multipleOf(5);
v.number().coerce(); // 允许把 "42" 转成 42(表单场景)
v.boolean().coerce(); // 'true'/'1'/1 → true,'false'/'0'/0 → false
v.bigint().min(0n).max(10n);
v.date().min(new Date('2026-01-01'));
v.date().coerce(); // ISO 字符串 / 时间戳 → Dateconst Order = v.object({
id: v.string().uuid(),
items: v.array(
v.object({ sku: v.string(), qty: v.number().int().min(1) }),
).min(1),
coupon: v.string().optional(),
});
// 未知键策略
v.object({ a: v.string() }); // 默认 strip:剔除多余键
v.object({ a: v.string() }).strict(); // strict:出现多余键直接报错
v.object({ a: v.string() }).passthrough(); // passthrough:原样保留
// 对象派生
Order.partial(); // 全部字段可选(PATCH 场景)
Order.pick(['id']); // 挑选字段
Order.omit(['coupon']);
Order.extend({ memo: v.string() });v.union([v.literal('text'), v.object({ kind: v.literal('img'), url: v.string().url() })]);
v.intersection(
v.object({ a: v.string() }),
v.object({ b: v.number() }),
);
v.tuple([v.string(), v.number()]); // 定长、位置类型固定
v.record(v.number()); // Record<string, number>
v.record(v.number(), v.string().uuid()); // 自定义键模式// 同步 refine
const Even = v.number().refine((n) => n % 2 === 0, '必须是偶数');
// 异步 refineAsync(需要 parseAsync / safeParseAsync)
const AvailableName = v.string().refineAsync(async (name) => {
const res = await fetch(`/api/check?name=${name}`);
return (await res.json()).available;
}, '用户名已被占用');
await AvailableName.safeParseAsync('lobster');
// transform:改变输出类型
v.string().transform((s) => s.trim().length);
// preprocess:解析前先处理输入
v.preprocess((i) => (typeof i === 'string' ? i.trim() : i), v.string().min(2));const Form = v.object({
keyword: v.coerce.string(),
page: v.coerce.number().int().min(1),
agree: v.coerce.boolean(),
at: v.coerce.date(),
amount: v.coerce.bigint(),
});
// URLSearchParams / FormData 取出的全是字符串,可直接交给 Form.parse// 方式一:单次解析指定
v.string().email().safeParse('bad', { locale: 'zh-CN' });
// → "value 必须是合法的电子邮箱"
// 方式二:全局切换
v.setLocale('zh-CN');
// 方式三:覆盖某个错误码的模板(字段名通过 {field} 注入)
v.object({ email: v.string().email() }).safeParse(raw, {
messages: {
required: (m) => `【${m.field}】不能为空`,
invalid_type: (m) => `【${m.field}】类型错误,应为 ${m.expected}`,
},
});错误对象结构(可直接渲染到表单字段旁):
interface Issue {
code: 'required' | 'invalid_type' | 'too_small' | 'invalid_format' | ...;
path: (string | number)[]; // 例如 ['items', 0, 'qty']
message: string;
meta?: Record<string, string | number | boolean>;
}v.toJsonSchema(v.object({
id: v.string().uuid(),
age: v.number().int().min(0).optional(),
email: v.string().email().nullable(),
}));
// {
// type: 'object',
// required: ['id', 'email'],
// properties: {
// id: { type: 'string', format: 'uuid' },
// age: { type: 'integer', minimum: 0 },
// email: { type: ['string', 'null'], format: 'email' }
// }
// }const LoginForm = v.object({
account: v.string().min(1, '请输入账号'),
password: v.string().min(8, '密码至少 8 位'),
});
function onSubmit(e) {
const r = LoginForm.safeParse(values, { locale: 'zh-CN', abortEarly: true });
if (!r.success) setError(r.issues[0].message);
}🖼️ 演示动图/截图占位:
docs/demo.gif(终端与表单场景的实际运行效果将在此展示)。
- 代码即单一事实来源(Single Source of Truth):一份链式声明同时产出运行时校验逻辑与 TS 静态类型,杜绝“接口定义写两遍、两边对不上”。
- 默认友好,而非默认裸奔:错误信息、中文格式、类型收窄这些 90% 项目都需要的能力直接内建。
- 零依赖即安全与稳定:没有供应链风险、没有版本冲突、没有安装陷阱;库本身可被浏览器、Node、BFF、CLI 任意场景嵌入。
- 错误结构化优先:错误不是拼好的字符串而已,
code + path + meta结构化输出,UI 渲染、日志分析、国际化二次加工都方便。
- TypeScript 7 strict:从实现侧保证 API 类型严谨;
const类型参数保证字面量不被宽化。 - ESM + CJS 双产物:
exports条件导出同时服务现代打包器与老项目 CommonJS;类型声明单独输出到dist/types。 - Node 内置
node:test:测试框架也保持零依赖,npm run verify一条命令完成构建、类型自检、单测与双模块冒烟。
- v1.1:
v.errorMap()支持日语 / 韩语等更多 locale;错误信息支持模板插值扩展 - v1.2:
v.email()国际化域名(Punycode)、v.string().byteLength()字节长度 - v1.3:JSON Schema 导入(Draft-07 → validkit 模式),与导出形成闭环
- v1.4:Deno / Bun / Edge Runtime 官方兼容测试矩阵
- v2.0:插件化自定义类型注册表与性能基准(benchmark)报告
🐛 边界 Bug 报告 · 🌐 新 locale 翻译 · 🧾 更多国家/地区的证件与号码格式 · 📚 框架集成示例(Vue / Solid / Fastify / Hono)。
validkit 属于工具库(library),无需下载可执行文件,安装后直接引入即可。
git clone https://github.com/gitstq/validkit.git
cd validkit
npm install # 仅安装开发依赖(typescript / @types/node)
npm run build # 产出 dist/esm、dist/cjs、dist/types
npm run verify # 构建 + 类型自检 + 82 项单测 + ESM/CJS/CLI 冒烟构建产物说明:
| 路径 | 内容 | 用途 |
|---|---|---|
dist/esm/ |
ESM 模块(含 package.json: {"type":"module"}) |
import / 现代打包器 |
dist/cjs/ |
CommonJS 模块 + 可执行 CLI | require / npx validkit |
dist/types/ |
.d.ts 类型声明 |
TypeScript 智能提示与类型推断 |
// ESM / TypeScript(推荐)
import { v } from 'validkit';
// CommonJS
const { v } = require('validkit');<!-- 浏览器:经 Vite/esbuild/webpack 打包后使用,无任何运行时外部依赖 -->| 环境 | 支持情况 |
|---|---|
| Node.js | ≥ 16.0.0(CJS / ESM 均测试通过) |
| 现代浏览器 | Chrome / Edge / Firefox / Safari 最近两个大版本(经打包器) |
| TypeScript | ≥ 5.0(类型推断体验最佳) |
| Deno / Bun | 可通过 npm 兼容层引入(官方矩阵在路线图中) |
Q:和 zod 是什么关系? A:产品形态受 zod 启发,但代码 100% 独立自研;差异点是内建中英双语、中文证件格式、零依赖双包与内置 CLI。
Q:为什么字符串长度对中文是正确的?
A:长度按 Unicode 码点(Array.from(str).length)计算,而不是 UTF-16 code unit,Emoji 也不会被算成 2。
Q:异步校验为什么没生效?
A:refineAsync 返回 Promise,需要使用 parseAsync / safeParseAsync 等待结果。
Q:default(() => value) 里的函数会被当成默认值本身吗?
A:不会。函数会被当作工厂调用(惰性求值),与 zod 行为一致;若确实需要默认值是一个函数,请再包一层 () => () => fn。
欢迎 PR!流程与规范详见 CONTRIBUTING.md:
- Fork 并新建
feat/xxx分支; npm install后开发,新增能力必须配套单元测试;- 提交信息遵循 Angular 规范:
feat: / fix: / docs: / refactor: / test: / chore:; - 提交前执行
npm run verify,确保构建、类型、测试、冒烟全绿。
Issue 反馈请提供最小复现;安全问题请勿公开 Issue。
本项目基于 MIT License 开源,可自由用于个人与商业项目,保留版权声明即可。
⭐ 如果 validkit 帮你少写了一个校验工具函数,欢迎点个 Star 支持!