自定义错误

TanStack Form 在您可以从验证器返回的错误值类型方面提供了完全的灵活性。字符串错误是最常见且易于使用的,但该库允许您从验证器返回任何类型的值。

作为一般规则,任何真值都被视为错误,并将表单或字段标记为无效,而假值(falseundefinednull 等)意味着没有错误,表单或字段是有效的。

从表单返回字符串值

tsx
<form.Field
  name="username"
  validators={{
    onChange: ({ value }) =>
      value.length < 3 ? '用户名必须至少包含 3 个字符' : undefined,
  }}
/>
<form.Field
  name="username"
  validators={{
    onChange: ({ value }) =>
      value.length < 3 ? '用户名必须至少包含 3 个字符' : undefined,
  }}
/>

对于影响多个字段的表单级验证:

tsx
const form = useForm({
  defaultValues: {
    username: '',
    email: '',
  },
  validators: {
    onChange: ({ value }) => {
      return {
        fields: {
          username:
            value.username.length < 3 ? '用户名太短' : undefined,
          email: !value.email.includes('@') ? '无效的电子邮件' : undefined,
        },
      }
    },
  },
})
const form = useForm({
  defaultValues: {
    username: '',
    email: '',
  },
  validators: {
    onChange: ({ value }) => {
      return {
        fields: {
          username:
            value.username.length < 3 ? '用户名太短' : undefined,
          email: !value.email.includes('@') ? '无效的电子邮件' : undefined,
        },
      }
    },
  },
})

字符串错误是最常见的类型,可以轻松在您的 UI 中显示:

tsx
{
  field.state.meta.errors.map((error, i) => (
    <div key={i} className="error">
      {error}
    </div>
  ))
}
{
  field.state.meta.errors.map((error, i) => (
    <div key={i} className="error">
      {error}
    </div>
  ))
}

数字

用于表示数量、阈值或大小:

tsx
<form.Field
  name="age"
  validators={{
    onChange: ({ value }) => (value < 18 ? 18 - value : undefined),
  }}
/>
<form.Field
  name="age"
  validators={{
    onChange: ({ value }) => (value < 18 ? 18 - value : undefined),
  }}
/>

Display in UI:

tsx
{
  /* TypeScript knows the error is a number based on your validator */
}
;<div className="error">
  您还需要 {field.state.meta.errors[0]} 年才能符合条件
</div>
{
  /* TypeScript knows the error is a number based on your validator */
}
;<div className="error">
  您还需要 {field.state.meta.errors[0]} 年才能符合条件
</div>

布尔值

用于指示错误状态的简单标志:

tsx
<form.Field
  name="accepted"
  validators={{
    onChange: ({ value }) => (!value ? true : undefined),
  }}
/>
<form.Field
  name="accepted"
  validators={{
    onChange: ({ value }) => (!value ? true : undefined),
  }}
/>

Display in UI:

tsx
{
  field.state.meta.errors[0] === true && (
    <div className="error">您必须接受条款</div>
  )
}
{
  field.state.meta.errors[0] === true && (
    <div className="error">您必须接受条款</div>
  )
}

对象

具有多个属性的丰富错误对象:

tsx
<form.Field
  name="email"
  validators={{
    onChange: ({ value }) => {
      if (!value.includes('@')) {
        return {
          message: '无效的电子邮件格式',
          severity: 'error',
          code: 1001,
        }
      }
      return undefined
    },
  }}
/>
<form.Field
  name="email"
  validators={{
    onChange: ({ value }) => {
      if (!value.includes('@')) {
        return {
          message: '无效的电子邮件格式',
          severity: 'error',
          code: 1001,
        }
      }
      return undefined
    },
  }}
/>

Display in UI:

tsx
{
  typeof field.state.meta.errors[0] === 'object' && (
    <div className={`error ${field.state.meta.errors[0].severity}`}>
      {field.state.meta.errors[0].message}
      <small> (Code: {field.state.meta.errors[0].code})</small>
    </div>
  )
}
{
  typeof field.state.meta.errors[0] === 'object' && (
    <div className={`error ${field.state.meta.errors[0].severity}`}>
      {field.state.meta.errors[0].message}
      <small> (Code: {field.state.meta.errors[0].code})</small>
    </div>
  )
}

在上面的示例中,这取决于您想要显示的事件错误。

数组

单个字段的多个错误消息:

tsx
<form.Field
  name="password"
  validators={{
    onChange: ({ value }) => {
      const errors = []
      if (value.length < 8) errors.push('密码太短')
      if (!/[A-Z]/.test(value)) errors.push('缺少大写字母')
      if (!/[0-9]/.test(value)) errors.push('缺少数字')

      return errors.length ? errors : undefined
    },
  }}
/>
<form.Field
  name="password"
  validators={{
    onChange: ({ value }) => {
      const errors = []
      if (value.length < 8) errors.push('密码太短')
      if (!/[A-Z]/.test(value)) errors.push('缺少大写字母')
      if (!/[0-9]/.test(value)) errors.push('缺少数字')

      return errors.length ? errors : undefined
    },
  }}
/>

Display in UI:

tsx
{
  Array.isArray(field.state.meta.errors) && (
    <ul className="error-list">
      {field.state.meta.errors.map((err, i) => (
        <li key={i}>{err}</li>
      ))}
    </ul>
  )
}
{
  Array.isArray(field.state.meta.errors) && (
    <ul className="error-list">
      {field.state.meta.errors.map((err, i) => (
        <li key={i}>{err}</li>
      ))}
    </ul>
  )
}

The disableErrorFlat Prop on Fields

By default, TanStack Form flattens errors from all validation sources (onChange, onBlur, onSubmit) into a single errors array. The disableErrorFlat prop preserves the error sources:

tsx
<form.Field
  name="email"
  disableErrorFlat
  validators={{
    onChange: ({ value }) =>
      !value.includes('@') ? 'Invalid email format' : undefined,
    onBlur: ({ value }) =>
      !value.endsWith('.com') ? 'Only .com domains allowed' : undefined,
    onSubmit: ({ value }) => (value.length < 5 ? 'Email too short' : undefined),
  }}
/>
<form.Field
  name="email"
  disableErrorFlat
  validators={{
    onChange: ({ value }) =>
      !value.includes('@') ? 'Invalid email format' : undefined,
    onBlur: ({ value }) =>
      !value.endsWith('.com') ? 'Only .com domains allowed' : undefined,
    onSubmit: ({ value }) => (value.length < 5 ? 'Email too short' : undefined),
  }}
/>

Without disableErrorFlat, all errors would be combined into field.state.meta.errors. With it, you can access errors by their source:

tsx
{
  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>
  )
}
{
  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>
  )
}

This is useful for:

  • Displaying different types of errors with different UI treatments
  • Prioritizing errors (e.g., showing submission errors more prominently)
  • Implementing progressive disclosure of errors

Type Safety of errors and errorMap

TanStack Form provides strong type safety for error handling. Each key in the errorMap has exactly the type returned by its corresponding validator, while the errors array contains a union type of all the possible error values from all validators:

tsx
<form.Field
  name="password"
  validators={{
    onChange: ({ value }) => {
      // This returns a string or undefined
      return value.length < 8 ? 'Too short' : undefined
    },
    onBlur: ({ value }) => {
      // 这返回一个对象或 undefined
      if (!/[A-Z]/.test(value)) {
        return { message: '缺少大写字母', level: 'warning' }
      }
      return undefined
    },
  }}
  children={(field) => {
    // TypeScript 知道 errors[0] 可以是 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
  }}
/>
<form.Field
  name="password"
  validators={{
    onChange: ({ value }) => {
      // This returns a string or undefined
      return value.length < 8 ? 'Too short' : undefined
    },
    onBlur: ({ value }) => {
      // 这返回一个对象或 undefined
      if (!/[A-Z]/.test(value)) {
        return { message: '缺少大写字母', level: 'warning' }
      }
      return undefined
    },
  }}
  children={(field) => {
    // TypeScript 知道 errors[0] 可以是 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
  }}
/>

errorMap 属性也是完全类型化的,与您的验证函数的返回类型匹配:

tsx
// With disableErrorFlat
<form.Field
  name="email"
  disableErrorFlat
  validators={{
    onChange: ({ value }): string | undefined =>
      !value.includes("@") ? "无效的电子邮件" : undefined,
    onBlur: ({ value }): { code: number, message: string } | undefined =>
      !value.endsWith(".com") ? { code: 100, message: "错误的域名" } : undefined
  }}
  children={(field) => {
    // TypeScript 知道每个错误源的确切类型
    const onChangeError: string | undefined = field.state.meta.errorMap.onChange;
    const onBlurError: { code: number, message: string } | undefined = field.state.meta.errorMap.onBlur;

    return (/* ... */);
  }}
/>
// With disableErrorFlat
<form.Field
  name="email"
  disableErrorFlat
  validators={{
    onChange: ({ value }): string | undefined =>
      !value.includes("@") ? "无效的电子邮件" : undefined,
    onBlur: ({ value }): { code: number, message: string } | undefined =>
      !value.endsWith(".com") ? { code: 100, message: "错误的域名" } : undefined
  }}
  children={(field) => {
    // TypeScript 知道每个错误源的确切类型
    const onChangeError: string | undefined = field.state.meta.errorMap.onChange;
    const onBlurError: { code: number, message: string } | undefined = field.state.meta.errorMap.onBlur;

    return (/* ... */);
  }}
/>

这种类型安全有助于在编译时而不是运行时捕获错误,使您的代码更可靠和可维护。