文档

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

失败收集整棵树的所有错误(不在第一个错误处中断),每条 Issuepath + message

默认忽略多余 key;要拒绝就 strict 收紧:

import { strict } from 'schema'

const Exact = strict({ name: string, "age?": number })
parse(Exact, { name: "Ann", role: "x" })   // FAIL  role: unexpected key

strict 只作用本层,类型上透明(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。
  • transform fn 由你保证纯函数性(无副作用);它在校验通过后才跑。
  • default 只在值缺失(undefined)时填;present-but-invalid 仍照常报错(要兜坏值用 transform)。默认值由构造期类型保证,不再过校验。

零运行时依赖。