TanStack Form 的 FieldApiOptions 详解:字段级配置的 9 个属性与 14 个类型参数全解析
2026/9/17 2:01:29 网站建设 项目流程

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接口的继承结构、全部类型参数与属性语义。读完本文,你将清楚每个字段选项(如defaultValueasyncDebounceMsasyncAlwaysdefaultMetadisableErrorFlat)在运行时到底如何生效、各框架适配器(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本身没有新增任何属性,它是两个接口的合并:

  1. FieldLikeApiOptions(定义于packages/form-core/src/types.ts):提供formnamedefaultValueasyncDebounceMsasyncAlwaysdefaultMetadisableErrorFlat这 7 个属性,并继续继承 FieldLikeOptions;
  2. FieldExtraOptions:提供validatorslisteners两个字段。

FieldLike*这套接口同时服务于FieldApiFormGroupApi(字段组),这正是两者共享name/form/defaultValue等基础选项的原因。所有类型参数都使用了in out双向方差修饰符,从源码结构看,这是为了让泛型在"读选项"和"写选项"两个方向上都不触发逆变冲突,从而保证适配器(如useField)在透传选项时类型推断不会退化。

二、类型参数逐一解读

参考文档列出了 14 个类型参数,它们并非都需要手动指定——绝大多数由FormApi与字段名自动推导。按职责可分为三组:

数据形状三参数

类型参数约束含义
TParentData父表单的数据类型(即FormApi<TParentData>TParentData
TNameextends DeepKeys<TParentData>字段名。被约束为父数据类型的深层键(如'address.city''items.0.name'),拼错字段名会直接报类型错误
TDataextends DeepValue<TParentData, TName>字段值类型,由TNameTParentData中解析出来

DeepKeysDeepValue定义在 util-types.ts,对应参考文档 DeepKeys 与 DeepValue。例如对type Data = { user: { email: string } }TName允许'user.email',此时TData自动推导为string

字段级校验器泛型(12 个TOn*

TOnMountTOnChangeTOnChangeAsyncTOnBlurTOnBlurAsyncTOnSubmitTOnSubmitAsyncTOnDynamicTOnDynamicAsync九个参数约束为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*

TFormOnMountTFormOnChangeTFormOnChangeAsyncTFormOnBlurTFormOnBlurAsyncTFormOnSubmitTFormOnSubmitAsyncTFormOnDynamicTFormOnDynamicAsyncTFormOnServer约束为undefined | FormValidateOrFn<TParentData>undefined | FormAsyncValidateOrFn<TParentData>。它们描述的是父表单在各级校验源上的类型,用于让字段的校验结果与表单级校验结果共同推导出TParentSubmitMeta(提交时返回给onSubmit回调的 meta 类型)。注意TFormOnServer专门对应server校验源,从源码注释看,它面向 SSR/SSG 场景的校验,不执行任何前端逻辑(见 types.ts#L39-L49)。

三、属性详解:每个选项的运行时行为

参考文档列出的属性共 9 个。以下逐一说明,并给出源码中实际的生效位置。

属性类型必填说明
formFormApi<TParentData, ...>字段所属的父表单实例
nameTNameDeepKeys<TParentData>字段名,深层键
defaultValueNoInfer<TData>字段默认值
asyncDebounceMsnumber异步校验的默认节流时间(毫秒)
asyncAlwaysboolean同步校验出错时是否仍执行异步校验
defaultMetaPartial<FieldLikeMeta<...>>字段元数据的默认值对象
disableErrorFlatboolean禁用对errorsflat(1)展开
validatorsFieldValidators<...>各校验源的校验器集合
listenersFieldListeners<...>各事件的监听器集合

form 与 name:字段的"身份证"

form是字段与父表单的唯一绑定,name是字段在该表单数据中的深层键路径。在FieldApi构造函数中(FieldApi.ts#L712-L741),二者被直接提升为实例属性:

constructor(opts: FieldApiOptions<...>) { this.form = opts.form this.name = opts.name this.options = opts // ... }

后续getFieldValuegetFieldMeta、校验、提交等所有操作都通过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 }

生效条件有四:字段未被触碰(isTouchedfalse)、表单中当前值为undefineddefaultValue本身不是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

三条规则值得注意:

  1. 校验器级优先validators.onChangeAsyncDebounceMs等会覆盖选项级的asyncDebounceMs
  2. submit 永不节流:无论从哪个方向推导,submit源的异步校验debounceMs都强制为0,保证提交时校验立即完成;
  3. 不设置则为 0asyncDebounceMs ?? 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<...>>,用于在字段初始化时预置元数据(如初始errorsisTouched等)。它的合并顺序在构造函数中(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)非空),则完全沿用已有 metadefaultMeta仅在字段首次创建时生效。基础默认值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.errorsflat(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,用于跨字段联动校验(如"确认密码"监听"密码");
  • 同步拒绝 PromiseRejectPromiseValidator会在编译期禁止把返回 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节流。onUnmountonGroupSubmit(所属字段组提交时触发)是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.valuefield.state.meta)由构造函数内的createStore驱动(FieldApi.ts#L749),store 每次重算都会先读取this.form.store触发对表单状态的订阅,再合并defaultFieldMetaopts.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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询