Skip to content

指南 · 进阶

版本基线 Valibot 1.x。本篇覆盖真实业务里的硬骨头:递归结构、判别联合、跨字段校验、对象工具方法、错误处理与 i18n、异步校验。

一、递归与自引用:lazy

递归结构(树、嵌套评论)会遇到「定义自己时引用自己」的循环,用 v.lazy(() => Schema) 延迟求值打破它:

ts
import * as v from 'valibot';

interface Category {
  name: string;
  children: Category[];
}

const CategorySchema: v.GenericSchema<Category> = v.object({
  name: v.string(),
  children: v.array(v.lazy(() => CategorySchema)), // 延迟引用自己
});

lazy 接收一个返回 schema 的函数,到校验时才求值,因此能引用尚未完成定义的自己。

二、判别联合:variant vs union

ts
// union:通用联合,逐个尝试,无判别键
const A = v.union([v.string(), v.number()]);

// variant:按判别字段分流,更高效、报错更准(≈ Zod discriminatedUnion)
const Shape = v.variant('type', [
  v.object({ type: v.literal('circle'), radius: v.number() }),
  v.object({ type: v.literal('rect'), width: v.number(), height: v.number() }),
]);

对象联合优先用 variant:它直接看 type 字段选中对应分支,比 union 挨个试快,错误信息也精准定位到具体分支。

三、跨字段校验:forward + partialCheck

「确认密码必须等于密码」这类跨字段校验,要把 action 放在对象 schema 之后的 pipe 里,用 forward 把 issue 转发到目标字段:

ts
const RegisterSchema = v.pipe(
  v.object({
    password: v.pipe(v.string(), v.minLength(8)),
    confirm: v.string(),
  }),
  v.forward(
    v.partialCheck(
      [['password'], ['confirm']],          // 关注的字段路径
      (input) => input.password === input.confirm,
      '两次输入的密码不一致'
    ),
    ['confirm']                              // issue 指到 confirm 字段
  )
);
  • partialCheck 拿到相关字段做布尔判断;
  • forward 把产生的 issue「转发」到指定路径,从而让表单在 confirm 下展示错误。

单字段自身的 pipe 看不到兄弟字段,所以跨字段校验必须放对象之后。

四、对象工具方法(对位 TS 工具类型)

ts
const User = v.object({ id: v.number(), name: v.string(), age: v.number() });

v.pick(User, ['id', 'name']);   // 取子集(≈ Pick)
v.omit(User, ['age']);          // 去掉某些键(≈ Omit)
v.partial(User);                // 全部可选(≈ Partial)
v.required(v.partial(User));    // 全部必填(≈ Required)
v.keyof(User);                  // 键名联合 picklist(['id','name','age'])

它们都以 schema 为第一个参数,返回新 schema,便于从一份基础 schema 派生出创建/更新等多种形态。

五、兜底与默认值方法

ts
// fallback:任意校验失败时回退(≈ Zod 的 catch)
const Str = v.fallback(v.string(), 'hello');
v.parse(Str, 123); // 'hello'

// 取默认值(不解析数据,常用于初始化表单)
const Form = v.object({
  theme: v.optional(v.picklist(['light', 'dark']), 'light'),
});
v.getDefaults(Form); // { theme: 'light' }

区分:fallback 针对任意非法输入兜底;optional 的默认值只在缺省/undefined 时生效。

六、错误处理与 i18n

ts
const result = v.safeParse(Schema, data);
if (!result.success) {
  // 扁平 issue → 按字段路径分组,便于表单展示
  const flat = v.flatten(result.issues);
  // { root?: [...], nested?: { 'user.email': [...] } }

  // 人类可读的汇总文本
  const text = v.summarize(result.issues);
}

自定义消息有两层:

ts
// 逐条:作为 schema/action 的最后一个参数
v.pipe(v.string(), v.minLength(8, '至少 8 位'));

// 全局/多语言:setGlobalMessage / setSpecificMessage / setSchemaMessage
v.setGlobalMessage((issue) => `校验失败:${issue.message}`);

七、异步校验

涉及数据库查询、远程接口的校验要用 Async 版本——约定是「同名 + Async 后缀」:

ts
import * as v from 'valibot';
import { isUsernameAvailable } from './api';

const ProfileSchema = v.objectAsync({
  username: v.pipeAsync(
    v.string(),
    v.checkAsync(isUsernameAvailable, '用户名已被占用') // 返回 Promise<boolean>
  ),
  avatar: v.pipe(v.string(), v.url()),                  // 同步项照常
});

const result = await v.safeParseAsync(ProfileSchema, data);

两条铁律:

  1. 异步函数只能嵌套在异步函数里;同步函数可以嵌进异步(反之不行)。所以一旦某层异步,外层的 object/pipe/解析方法都要换成 objectAsync/pipeAsync/parseAsync
  2. 官方建议「能同步就同步,只把必须异步的部分换成 Async」,以控制复杂度与体积。

进入 指南 · 专家:体积与 tree-shaking 真相、safeParse 的 typed 三态、parser 预编译、brand 名义类型、从 Zod 迁移取舍。