schema
字面量即 Schema。 Schema 是惰性裸数据,动词全是外置函数。写像 TS interface,组合像普通 JS。
// 你脑子里想的(TS interface)
interface User {
name: string
age: number
email?: string
}
// 这个库(把 interface 直接翻译成裸对象字面量)
import { string, number, type Infer } from 'schema'
const User = {
name: string,
age: number,
"email?": string.email(),
}
type User = Infer<typeof User> // { name: string; age: number; email?: string }对比 Zod 多出来的 3 层噪音(z.、.object()、.optional())全没了:
const User = z.object({
name: z.string(),
age: z.number(),
email: z.string().email().optional(),
})#三件事是分开的
| 怎么写 | 何时 | |
|---|---|---|
| 值 | const User = { name: string, "email?": string } | 裸字面量,普通 JS 对象 |
| 类型 | type User = Infer<typeof User> | 编译期,零运行时 |
| 校验 | parse(User, data) / safeParse(User, data) / check(User, data) | 运行时,外置函数吃 schema |
Schema 是数据不是对象,所以 JS 的一切原生可用 —— 不需要 .extend()/.merge()/.pick()/.omit():
const Timestamps = { createdAt: date, "deletedAt?": date }
const Post = {
title: string.min(1),
...Timestamps, // 直接 spread 组合
}
const Admin = {
...User,
role: literal("admin"), // 直接覆写字段
}#校验
import { string, number, parse, safeParse, check } from 'schema'
const User = { name: string.min(1), age: number.min(0), "email?": string.email() }
parse(User, input) // 通过返回 typed data,失败 throw SchemaError
const r = safeParse(User, input) // { success: true, data } | { success: false, error }
if (check(User, input)) input.name // 类型守卫,input 收窄为 User失败收集整棵树的所有错误(不在第一个错误处中断),每条 Issue 带 path + message。
默认忽略多余 key;要拒绝就 strict 收紧:
import { strict } from 'schema'
const Exact = strict({ name: string, "age?": number })
parse(Exact, { name: "Ann", role: "x" }) // FAIL role: unexpected keystrict 只作用本层,类型上透明(Infer<strict(S)> === Infer<S>)。
#refine / transform
refine 加自定义谓词(不改类型);transform 校验后变换值(Output 可异于 Input):
import { string, number, transform } from 'schema'
const Even = number.refine(n => n % 2 === 0, 'must be even') // 链式,不污染类型
const Trimmed = string.transform(s => s.trim()) // string → string
const Length = string.transform(s => s.length) // string → number,Output≠Input
parse(Length, 'hello') // 5(产出变换后的值)
transform(array(string), xs => xs.join(',')) // 复合上也能 transform校验失败时不调 transform fn(值不可信)。无变换的字段/元素保持引用,
parse 成功返回原对象(只在真正变换处浅拷贝,多余 key 透传不剥离)。
Infer<S> 取 Output;新增 InferInput<S> 取 Input(transform 的入参类型)。
#default
default 与 transform 互为镜像:transform 让 Output 偏离 Input,default 让 Input 偏离 Output——缺值(undefined)时填默认值,Input 端可省略,Output 端必有:
import { string, number, default_ } from 'schema'
const Role = string.default('user') // leaf 便捷方法
parse(Role, undefined) // 'user'(缺则填)
parse(Role, 'admin') // 'admin'(给则校验)
parse(Role, 42) // FAIL(default 只兜「缺」,不兜「坏」)
// 对象字段 default:缺 key 自动回填,无需写 "key?"
const Cfg = { name: string, role: default_(string, 'user'), retries: default_(number, 3) }
parse(Cfg, { name: 'a' }) // { name: 'a', role: 'user', retries: 3 }
// Infer = { name: string; role: string; retries: number } ← Output 必有
// InferInput = { name: string; role?: string; retries?: number } ← Input 可省略default_(array(string), []) 对复合 schema 同样可用。
#复合类型
import { array, union, tuple, record, literal, enum_, lazy } from 'schema'
array(string.min(1))
union(literal("active"), literal("inactive")) // 变长参数
tuple(string, number) // 变长参数
record(number)
enum_(["admin", "user"]) // 数组参数
// 递归(JS 字面量无法自引用,lazy 是唯一逃逸舱)
const Tree = { value: string, "children?": array(lazy(() => Tree)) }#生态:Standard Schema
实现了 Standard Schema ~standard 接口,自动接入 tRPC / react-hook-form / TanStack。叶子 schema(string 等)天然兼容;裸对象 shape 在生态边界用一次 standard() 包:
import { standard } from 'schema'
t.procedure.input(standard(User))
useForm({ resolver: standardSchemaResolver(standard(User)) })其余地方 User 永远是裸字面量。
#已知边界(Phase 1)
- Schema key 中以
?结尾恒被当作可选标记 —— 数据 key 真以?结尾会歧义。 lazy对循环数据(非循环 schema)无 seen-set 守卫,会栈溢出(同 Zod)。- 多余 key 默认忽略(同 Zod 默认);
strict(shape)收紧为闭集,schema 外的 key 报unexpected key(只作用本层,嵌套需逐层strict)。 union全败时报 issue 最少的那支(best-match),判别联合下错落在判别字段;而非笼统 no member matched。transformfn 由你保证纯函数性(无副作用);它在校验通过后才跑。default只在值缺失(undefined)时填;present-but-invalid 仍照常报错(要兜坏值用 transform)。默认值由构造期类型保证,不再过校验。
零运行时依赖。