TanStack Form 的 FieldApiOptions 详解:字段级配置的 9 个属性与 14 个类型参数全解析
【免费下载链接】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
本文基于 TanStack Form 官方参考文档 FieldApiOptions,结合form-core包的源码实现,完整拆解FieldApiOptions接口的继承结构、全部类型参数与属性语义。读完本文,你将清楚每个字段选项(如defaultValue、asyncDebounceMs、asyncAlways、defaultMeta、disableErrorFlat)在运行时到底如何生效、各框架适配器(useField/createField等)如何透传这些选项,以及如何在 React 等框架中写出类型安全的字段配置。
一、FieldApiOptions 是什么,在哪里定义
FieldApiOptions是创建FieldApi(表单字段 API 实例)时传入的配置对象类型。它是 TanStack Form 中"字段"这一抽象的完整契约:绑定哪个父表单、字段叫什么名字、默认值是什么、校验器与监听器如何挂载、异步校验的节流策略如何设定。
该接口定义在 FieldApi.ts#L383,源码声明如下(节选):
export interface FieldApiOptions< in out TParentData, in out TName extends DeepKeys<TParentData>, in out TData extends DeepValue<TParentData, TName>, in out TOnMount extends undefined | FieldValidateOrFn<TParentData, TName, TData>, in out TOnChange extends undefined | FieldValidateOrFn<TParentData, TName, TData>, // ... 省略其余校验器泛型参数 in out TFormOnServer extends undefined | FormAsyncValidateOrFn<TParentData>, in out TParentSubmitMeta, > extends FieldLikeApiOptions<...>, // 基础字段选项(name、defaultValue 等)+ form 绑定 FieldExtraOptions<...> // validators 与 listeners {}从源码结构看,FieldApiOptions本身没有新增任何属性,它是两个接口的合并:
- FieldLikeApiOptions(定义于
packages/form-core/src/types.ts):提供form、name、defaultValue、asyncDebounceMs、asyncAlways、defaultMeta、disableErrorFlat这 7 个属性,并继续继承 FieldLikeOptions; - FieldExtraOptions:提供
validators与listeners两个字段。
FieldLike*这套接口同时服务于FieldApi与FormGroupApi(字段组),这正是两者共享name/form/defaultValue等基础选项的原因。所有类型参数都使用了in out双向方差修饰符,从源码结构看,这是为了让泛型在"读选项"和"写选项"两个方向上都不触发逆变冲突,从而保证适配器(如useField)在透传选项时类型推断不会退化。
二、类型参数逐一解读
参考文档列出了 14 个类型参数,它们并非都需要手动指定——绝大多数由FormApi与字段名自动推导。按职责可分为三组:
数据形状三参数
| 类型参数 | 约束 | 含义 |
|---|---|---|
TParentData | 无 | 父表单的数据类型(即FormApi<TParentData>的TParentData) |
TName | extends DeepKeys<TParentData> | 字段名。被约束为父数据类型的深层键(如'address.city'或'items.0.name'),拼错字段名会直接报类型错误 |
TData | extends DeepValue<TParentData, TName> | 字段值类型,由TName从TParentData中解析出来 |
DeepKeys与DeepValue定义在 util-types.ts,对应参考文档 DeepKeys 与 DeepValue。例如对type Data = { user: { email: string } },TName允许'user.email',此时TData自动推导为string。
字段级校验器泛型(12 个TOn*)
TOnMount、TOnChange、TOnChangeAsync、TOnBlur、TOnBlurAsync、TOnSubmit、TOnSubmitAsync、TOnDynamic、TOnDynamicAsync九个参数约束为undefined | FieldValidateOrFn<TParentData, TName, TData>,异步版本约束为undefined | FieldAsyncValidateOrFn<...>。
关键在于FieldValidateOrFn是一个联合类型(见 FieldApi.ts#L160):
export type FieldValidateOrFn<TParentData, TName, TData> = | FieldValidateFn<TParentData, TName, TData> // (props: { value, fieldApi }) => unknown | StandardSchemaV1<TData, unknown> // Standard Schema v1 验证器(如 Zod 的 z.string().min(1))也就是说校验器既可以是接收{ value, fieldApi }的普通函数,也可以是任何实现了 Standard Schema v1 协议的验证器实例(如z.string().refine(...))。这些泛型的真实作用是"回传":当你在validators.onChange里传入z.string().min(1)后,state.meta.errors的类型就能精确到具体错误类型,而不是退化为unknown。
表单级校验器泛型(9 个TFormOn*)
TFormOnMount、TFormOnChange、TFormOnChangeAsync、TFormOnBlur、TFormOnBlurAsync、TFormOnSubmit、TFormOnSubmitAsync、TFormOnDynamic、TFormOnDynamicAsync、TFormOnServer约束为undefined | FormValidateOrFn<TParentData>或undefined | FormAsyncValidateOrFn<TParentData>。它们描述的是父表单在各级校验源上的类型,用于让字段的校验结果与表单级校验结果共同推导出TParentSubmitMeta(提交时返回给onSubmit回调的 meta 类型)。注意TFormOnServer专门对应server校验源,从源码注释看,它面向 SSR/SSG 场景的校验,不执行任何前端逻辑(见 types.ts#L39-L49)。
三、属性详解:每个选项的运行时行为
参考文档列出的属性共 9 个。以下逐一说明,并给出源码中实际的生效位置。
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
form | FormApi<TParentData, ...> | 是 | 字段所属的父表单实例 |
name | TName(DeepKeys<TParentData>) | 是 | 字段名,深层键 |
defaultValue | NoInfer<TData> | 否 | 字段默认值 |
asyncDebounceMs | number | 否 | 异步校验的默认节流时间(毫秒) |
asyncAlways | boolean | 否 | 同步校验出错时是否仍执行异步校验 |
defaultMeta | Partial<FieldLikeMeta<...>> | 否 | 字段元数据的默认值对象 |
disableErrorFlat | boolean | 否 | 禁用对errors的flat(1)展开 |
validators | FieldValidators<...> | 否 | 各校验源的校验器集合 |
listeners | FieldListeners<...> | 否 | 各事件的监听器集合 |
form 与 name:字段的"身份证"
form是字段与父表单的唯一绑定,name是字段在该表单数据中的深层键路径。在FieldApi构造函数中(FieldApi.ts#L712-L741),二者被直接提升为实例属性:
constructor(opts: FieldApiOptions<...>) { this.form = opts.form this.name = opts.name this.options = opts // ... }后续getFieldValue、getFieldMeta、校验、提交等所有操作都通过this.form+this.name定位字段,这也是框架适配器只需传入这两个参数即可完成字段注册的原因。
defaultValue:只在"未触碰且无值"时生效
defaultValue的类型是NoInfer<TData>——用NoInfer包裹是为了防止默认值参与反向推导,避免它干扰TData从字段名推导出的类型。
它并非无条件覆盖表单值。在构造函数的 store 初始化逻辑中(FieldApi.ts#L780-L793):
let value = this.form.getFieldValue(this.name) if ( !meta.isTouched && (value as unknown) === undefined && this.options.defaultValue !== undefined && !evaluate(value, this.options.defaultValue) ) { value = this.options.defaultValue }生效条件有四:字段未被触碰(isTouched为false)、表单中当前值为undefined、defaultValue本身不是undefined、且evaluate判定两者不等值。换言之,defaultValue是"兜底值"而非"初始强制值",不会覆盖form.defaultValues中已经提供的值。这一点在 React 适配器的测试中被明确验证,例如 useField.test.tsx#L1694-L1703 中should allow field-level defaultValue用例:
<form.Field name="name" defaultValue="a">asyncDebounceMs 与 validators.*AsyncDebounceMs:两级节流
asyncDebounceMs是"默认节流时间",只有当校验器没有提供"更具体的节流时间"时才生效。完整的解析链在 utils.ts#L407-L458 中(getValidationLogicFn内部):
const { asyncDebounceMs } = options const { onBlurAsyncDebounceMs, onChangeAsyncDebounceMs, onDynamicAsyncDebounceMs } = (options.validators || {}) const defaultDebounceMs = asyncDebounceMs ?? 0 // 按校验源选择节流时间 switch (validatorCause) { case 'change': debounceMs = onChangeAsyncDebounceMs ?? defaultDebounceMs; break case 'blur': debounceMs = onBlurAsyncDebounceMs ?? defaultDebounceMs; break case 'dynamic': debounceMs = onDynamicAsyncDebounceMs ?? defaultDebounceMs; break case 'submit': debounceMs = 0; break // submit 校验始终立即执行 } if (cause === 'submit') debounceMs = 0三条规则值得注意:
- 校验器级优先:
validators.onChangeAsyncDebounceMs等会覆盖选项级的asyncDebounceMs; - submit 永不节流:无论从哪个方向推导,
submit源的异步校验debounceMs都强制为0,保证提交时校验立即完成; - 不设置则为 0:
asyncDebounceMs ?? 0,即默认不节流,异步校验随触发源即时运行。
React 测试 useField.test.tsx#L632 用onChangeAsyncDebounceMs: 100验证了节流后的调用次数,注释中明确写道"withoutonChangeAsyncDebounceMsmockFn will have been called 5 times",直观展示了节流对重复触发的抑制作用。
asyncAlways:同步出错时是否短路异步校验
asyncAlways(types.ts#L979-L982 的 JSDoc 描述):若为true,即使同步校验阶段已产生错误,也仍然执行异步校验。
默认行为是"短路"。在字段异步校验入口(FieldApi.ts#L1679):
if (hasErrored && !this.options.asyncAlways) { this.getInfo().validationMetaMap[getErrorMapKey(cause)]?.lastAbortController.abort() // 直接返回已有错误,不再发起异步校验 return [...this.state.meta.errors, ...groupErrors.flat()] }同样的短路逻辑同时存在于 FormGroupApi.ts#L2322 与 FormApi.ts#L1942、FormApi.ts#L2402,即字段、字段组、表单三层各自持有asyncAlways选项并独立判断。适用场景例如:同步校验检查"必填",异步校验做服务端唯一性检查——当你希望"即使为空也去服务端查一次占用状态"时,就设置asyncAlways: true。
defaultMeta:预置字段元数据
defaultMeta接受Partial<FieldLikeMeta<...>>,用于在字段初始化时预置元数据(如初始errors、isTouched等)。它的合并顺序在构造函数中(FieldApi.ts#L780-L783):
const meta = this.form.getFieldMeta(this.name) ?? { ...defaultFieldMeta, // 来自 packages/form-core/src/metaHelper.ts ...opts.defaultMeta, // 用户提供的 defaultMeta 覆盖默认值 }注意优先级:如果表单中已存在该字段的 meta(form.getFieldMeta(this.name)非空),则完全沿用已有 meta,defaultMeta仅在字段首次创建时生效。基础默认值defaultFieldMeta定义在 metaHelper.ts#L11-L24:
export const defaultFieldMeta: AnyFieldLikeMeta = { isValidating: false, isTouched: false, isBlurred: false, isDirty: false, isPristine: true, isValid: true, isDefaultValue: true, errors: [], errorMap: {}, errorSourceMap: {}, _arrayVersion: 0, _pendingValidationsCount: 0, }defaultMeta的典型用途是在服务端渲染或错误回填时,让字段一挂载就携带初始错误(参考文档中 React 的提交处理指南 演示了表单级 meta 回传的用法,字段级同理)。
disableErrorFlat:保留错误的原始结构
disableErrorFlat(types.ts#L1011-L1014)的 JSDoc 说明:禁用对field.errors的flat(1)操作,"这在希望保留错误原始结构时有用,但不建议大多数场景使用"。
其生效点在表单聚合字段错误的地方(FormApi.ts#L1228-L1229):
if (!fieldInstance || !fieldInstance.options.disableErrorFlat) { fieldErrors = fieldErrors.flat(1) }正常情况下字段错误会被展开一层(flat(1))成为扁平数组,方便 UI 直接遍历;设为true后保留unknown[][]的嵌套结构,从types.ts中的多处 TODO 注释看,这一分支后续计划支持返回StandardSchemaV1Issue[][]以保留 Standard Schema 验证器的完整 issue 结构(types.ts#L360)。
validators:挂载在六个校验源上的校验器
validators的类型是 FieldValidators(参考文档见 FieldValidators),源码注释完整列出了全部成员:
export interface FieldValidators<...> { onMount?: RejectPromiseValidator<TOnMount> // 挂载时运行 onChange?: RejectPromiseValidator<TOnChange> // 值变化时运行(如 z.string().min(1)) onChangeAsync?: TOnChangeAsync // 值变化时的异步校验 onChangeAsyncDebounceMs?: number // onChangeAsync 节流(ms) onChangeListenTo?: DeepKeys<TParentData>[] // 监听哪些字段变化来触发本字段的 onChange/onChangeAsync onBlur?: RejectPromiseValidator<TOnBlur> // 失焦时运行 onBlurAsync?: TOnBlurAsync onBlurAsyncDebounceMs?: number onBlurListenTo?: DeepKeys<TParentData>[] onSubmit?: RejectPromiseValidator<TOnSubmit> // 提交时运行 onSubmitAsync?: TSubmitAsync onDynamic?: RejectPromiseValidator<TOnDynamic> // 动态(值结构)校验源 onDynamicAsync?: TOnDynamicAsync onDynamicAsyncDebounceMs?: number }各校验源与内部ValidationCause('change' | 'blur' | 'submit' | 'mount' | 'server' | 'dynamic',见 types.ts#L43)一一对应。两个实用细节:
*ListenTo依赖监听:onChangeListenTo: ['lastName']表示当lastName变化时,重跑本字段的onChange/onChangeAsync,用于跨字段联动校验(如"确认密码"监听"密码");- 同步拒绝 Promise:
RejectPromiseValidator会在编译期禁止把返回 Promise 的函数放到同步校验位(如onChange),强制异步逻辑走onChangeAsync,避免"异步校验被当同步用"这一常见坑。
listeners:非校验的事件钩子
listeners的类型是 FieldListeners(参考文档见 FieldListeners):
export interface FieldListeners<TParentData, TName, TData> { onChange?: FieldListenerFn<TParentData, TName, TData> onChangeDebounceMs?: number onBlur?: FieldListenerFn<TParentData, TName, TData> onBlurDebounceMs?: number onMount?: FieldListenerFn<TParentData, TName, TData> onUnmount?: FieldListenerFn<TParentData, TName, TData> onSubmit?: FieldListenerFn<TParentData, TName, TData> onGroupSubmit?: FieldListenerFn<TParentData, TName, TData> }与validators的区别在于:listeners的回调签名是(props: { value, fieldApi }) => void,不产生错误,只用于副作用(记录日志、同步外部状态等),且onChange/onBlur支持独立的*DebounceMs节流。onUnmount与onGroupSubmit(所属字段组提交时触发)是validators所没有的时机。
四、实战:框架适配器如何使用 FieldApiOptions
文档说明"通常不需要直接new FieldApi",而是通过框架适配器创建。FieldApiOptions正是这些适配器的公共底座——React 的useField(useField.tsx)、Solid 的createField、Svelte 的Field.svelte等最终都会把 props 组装成一个FieldApiOptions传入构造函数。以官方示例 examples/react/simple/src/index.tsx 为例,把上面所有选项串起来:
import { useForm } from '@tanstack/react-form' function App() { const form = useForm({ defaultValues: { firstName: '' }, onSubmit: async ({ value }) => console.log(value), }) return ( <form.Field name="firstName" // name: DeepKeys<FormData> defaultValue="Tess" // defaultValue: NoInfer<TData> asyncDebounceMs={300} // 异步校验默认节流 300ms validators={{ onChange: ({ value }) => !value ? 'A first name is required' : value.length < 3 ? 'First name must be at least 3 characters' : undefined, onChangeAsyncDebounceMs: 500, // 覆盖 asyncDebounceMs onChangeAsync: async ({ value }) => { await new Promise((r) => setTimeout(r, 1000)) return value.includes('error') && 'No "error" allowed in first name' }, }} listeners={{ onChange: ({ value }) => console.log('typed:', value), }} > {(field) => ( <> <input id={field.name} name={field.name} value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} onBlur={field.handleBlur} /> {field.state.meta.isValidating && <em>Validating...</em>} {field.state.meta.errors.map((e) => <em key={String(e)}>{e}</em>)} </> )} </form.Field> ) }字段运行时状态(field.state.value与field.state.meta)由构造函数内的createStore驱动(FieldApi.ts#L749),store 每次重算都会先读取this.form.store触发对表单状态的订阅,再合并defaultFieldMeta与opts.defaultMeta得出当前 meta,实现了选项到响应式状态的单向数据流。
五、核心测试覆盖
FieldApiOptions各属性在核心测试中均有对应验证,可作为行为基准:
- 字段级
defaultValue:useField.test.tsx#L1694(should allow field-level defaultValue)与defaultValue不触发渲染期setState警告的用例(同文件 L1647); - 异步节流:useField.test.tsx#L632 的
onChangeAsyncDebounceMs: 100断言调用次数; - 字段 API 行为总集:FieldApi.spec.ts;
- 类型层面(含
FieldApiOptions泛型推导):FieldApi.test-d.ts 及 React 侧的 useField.test-d.tsx。
六、小结与相关文档
FieldApiOptions是 TanStack Form 字段体系的"配置总纲":form+name完成字段注册,defaultValue/defaultMeta控制初始状态,validators/listeners分别在六个校验源与四个事件时机上挂载逻辑,asyncDebounceMs/asyncAlways/disableErrorFlat三个开关精细调节异步校验与错误结构的行为。由于接口继承自FieldLikeApiOptions,本文描述的语义同样适用于FormGroupApi(字段组)的同名选项。
延伸阅读(均为仓库内文档):
- FieldApi 类参考
- FieldValidators 接口
- FieldListeners 接口
- FormApi 类参考
- FormGroupOptions 接口
- React 字段指南 与 安装指南
【免费下载链接】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),仅供参考