深入解读 TanStack Form 的ValidationError类型:为什么校验错误被设计为unknown
【免费下载链接】form🤖 Headless, performant, and type-safe form state management for TS/JS, React, Vue, Angular, Solid, and Lit.项目地址: https://gitcode.com/GitHub_Trending/form/form
导读
ValidationError是 TanStack Form(form-core 包)中定义的一个极其简洁却又贯穿全局的类型别名,它直接决定了整个表单校验体系对“错误值”的约束边界。本文以 ValidationError 类型别名文档 为核心,结合 form-core 源码 与官方 React 指南,深入讲解这一unknown类型的设计动机、它在字段级/表单级校验链路中的实际位置,以及开发者如何利用它写出类型安全且灵活的错误处理代码。
一、ValidationError的定义:一行代码背后的设计哲学
在 packages/form-core/src/types.ts 中,该类型的完整定义只有一行:
export type ValidationError = unknown与大多数表单库将错误类型硬编码为string | string[]不同,TanStack Form 刻意把错误值放宽为 TypeScript 的顶层类型unknown。这意味着:
- 校验函数可以返回任意类型的错误值:字符串、错误对象、错误码数组、
Standard Schema的 issue 列表,甚至undefined(表示校验通过); - 库本身不预设错误的数据结构:展示层如何渲染、国际化如何处理、错误如何分组,全部交由应用开发者决定;
- 对第三方校验库友好:Zod、Valibot、Yup 等 schema 库产生的错误结构各不相同,
unknown让 TanStack Form 无需为每个生态定制类型适配。
可以这样理解:ValidationError是 TanStack Form 校验体系里所有错误值的“基座类型”,它本身不做任何约束,真正的类型安全约束发生在每个校验函数(validator)的签名与errorMap/errors的推导类型上。
二、ValidationError在整个校验体系中的位置
2.1 字段级校验:校验函数返回unknown
字段级校验函数FieldValidateFn定义在 packages/form-core/src/FieldApi.ts,其返回类型直接对应ValidationError语义:
export type FieldValidateFn<TParentData, TName, TData> = (props: { value: TData fieldApi: FieldApi<...> }) => unknown异步版本FieldValidateAsyncFn同样如此(见 FieldApi.ts)。而实际执行校验的入口runValidator(FieldApi.ts)会把校验函数或 Standard Schema 校验器的返回结果统一收拢:
runValidator<...>(props: { validate, value, type }): unknown { if (isStandardSchemaValidator(props.validate)) { return standardSchemaValidatorsprops.type as never } return (props.validate as FieldValidateFn<any, any>)(props.value) as never }从源码结构可以看出:无论你用的是普通函数校验还是 Standard Schema 校验,最终产出的“原始错误”(raw error)都落在unknown这个基座上,随后由上层逻辑归一化后写入errorMap。
2.2 错误存储:errorMap与errors两个出口
错误值产生后,TanStack Form 提供两种读取方式,二者均由ValidationError派生:
方式一:errors(拍平后的错误数组)
field.state.meta.errors会把各触发时机(onMount/onChange/onBlur/onSubmit/onDynamic)收集到的错误拍平成数组,适合“只要有错就显示”的简单场景。
方式二:errorMap(按触发时机分组的错误字典)
对应类型ValidationErrorMap定义在 packages/form-core/src/types.ts,每个键对应一种校验触发时机:
export type ValidationErrorMap<...> = { onMount?: TOnMountReturn onChange?: TOnChangeReturn | TOnChangeAsyncReturn onBlur?: TOnBlurReturn | TOnBlurAsyncReturn onSubmit?: TOnSubmitReturn | TOnSubmitAsyncReturn onDynamic?: TOnDynamicReturn | TOnDynamicAsyncReturn onServer?: TOnServerReturn }底层实现中,异步校验结果会写入字段 meta 的errorMap[errorMapKey],并同步记录来源(字段级还是表单级),见 FieldApi.ts 中的field.setMeta调用,以及errorSourceMap(types.ts)对错误来源的追踪。
2.3 表单级错误:FormValidationError与GlobalFormValidationError
ValidationError也是表单级错误的基石。在 packages/form-core/src/types.ts 中:
export type FormValidationError<TFormData> = | ValidationError | GlobalFormValidationError<TFormData> export type GlobalFormValidationError<TFormData> = { form?: ValidationError fields: Partial<Record<DeepKeys<TFormData>, ValidationError>> }表单级校验可以返回一个全局错误(作用于整张表单),也可以返回一个按字段路径映射的错误字典(如{ form: '...', fields: { age: 'Must be 13 or older' } })。DeepKeys<TFormData>让字段路径具备编译期检查,防止拼错字段名。
三、实战:基于unknown的类型安全错误处理
ValidationError是unknown,并不意味着使用时要到处as any。恰恰相反,每个校验函数自身的返回类型会被精确推断,从而在编译期锁定错误结构。
3.1 不同校验函数返回不同错误结构
以官方 React 指南 custom-errors.md 的示例为骨架,一个字段的不同校验器可以返回字符串或对象:
<form.Field name="password" validators={{ onChange: ({ value }) => { // 返回 string 或 undefined return value.length < 8 ? 'Too short' : undefined }, onBlur: ({ value }) => { // 返回对象或 undefined if (!/[A-Z]/.test(value)) { return { message: 'Missing uppercase', level: 'warning' } } return undefined }, }} children={(field) => { // errors 数组是 string | { message: string, level: string } | undefined 的联合类型 const error = field.state.meta.errors[0] if (typeof error === 'string') { return <div className="string-error">{error}</div> } else if (error && typeof error === 'object') { return <div className={error.level}>{error.message}</div> } return null }} />由于错误值源自unknown基座,TypeScript 无法在未收窄前假定其形状,开发者必须通过typeof等收窄手段处理——这反而保证了运行时的稳健性,避免了“类型上说是 string,运行时却是对象”的隐患。
3.2 使用errorMap按触发时机精细化展示
结合disableErrorFlat选项,可以关闭errors的拍平行为,改从errorMap按来源精确取错,便于对不同校验时机做差异化 UI 处理(实时校验、失焦反馈、提交错误分层展示),示例见 custom-errors.md:
{ field.state.meta.errorMap.onChange && ( <div className="real-time-error">{field.state.meta.errorMap.onChange}</div> ) } { field.state.meta.errorMap.onBlur && ( <div className="blur-feedback">{field.state.meta.errorMap.onBlur}</div> ) } { field.state.meta.errorMap.onSubmit && ( <div className="submit-error">{field.state.meta.errorMap.onSubmit}</div> ) }errorMap的每个键都精确对应其校验函数的返回类型,TypeScript 可以直接给出string | undefined或{ code: number, message: string } | undefined这类推导(见 custom-errors.md 的完整示例),把错误结构错误提前到编译期暴露。
3.3 订阅与全局读取
ValidationError同样体现在表单级状态订阅中。在 basic-concepts.md 可以看到,通过useSelector订阅form.store的errorMap,可以拿到整个表单按字段组织、按时机分组的完整错误字典:
const errors = useSelector(form.store, (state) => state.errorMap)配合动态校验(onDynamic,见 dynamic-validation.md)和焦点管理(基于errorMap.onChange定位首个错误字段,见 focus-management.md),ValidationError支撑起了从单个字段到整张表单的完整错误可视化链路。
四、与 Standard Schema 生态的衔接
ValidationError的unknown设计,还让 TanStack Form 可以无缝接入 Standard Schema 生态。当校验器是符合 Standard Schema 的 schema 时,runValidator会走standardSchemaValidators[props.type]分支(FieldApi.ts),此时错误表现为StandardSchemaV1Issue[](issue 数组),其结构定义于 packages/form-core/src/standardSchemaValidator.ts。由于ValidationError是unknown,Zod/Valibot 等库产生的 issue 数组无需任何包装即可作为错误值流转。
此外,类型层面的UnwrapFieldValidateOrFn(types.ts)会识别“校验函数是否来自 Standard Schema”,自动把字段错误类型推导为StandardSchemaV1Issue[],让 schema 校验场景同样获得完整类型提示。
五、设计取舍小结
| 关注点 | ValidationError = unknown带来的收益 |
|---|---|
| 错误结构自由度 | 字符串、对象、数组、schema issue 均可作为错误值 |
| 生态兼容 | 无需为不同校验库做类型适配层 |
| 类型安全 | 约束下放到每个校验函数签名,配合errorMap/errors精确推导 |
| 运行时稳健 | 消费端必须显式收窄类型,避免虚假的类型承诺 |
同时也要注意它的代价:错误展示代码需要自己处理结构判断与收窄。这正是 TanStack Form “headless(无头)”理念的体现——库只负责状态、触发时机与存储,错误如何被消费完全交由开发者掌控。
六、延伸阅读
- 类型别名本体与周边类型:packages/form-core/src/types.ts
- 字段校验函数的定义与执行入口:packages/form-core/src/FieldApi.ts
- 字段级异步校验的 Promise 调度与
errorMap写入:packages/form-core/src/FieldApi.ts - 表单级错误结构
FormValidationError/GlobalFormValidationError:packages/form-core/src/types.ts - 错误定制与类型安全实战:docs/framework/react/guides/custom-errors.md
- Standard Schema 类型别名:docs/reference/type-aliases/StandardSchemaV1.md
【免费下载链接】form🤖 Headless, performant, and type-safe form state management for TS/JS, React, Vue, Angular, Solid, and Lit.项目地址: https://gitcode.com/GitHub_Trending/form/form
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考