form-validation

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Form Validation

表单验证

Schema-first validation using Zod as the single source of truth for both runtime validation and TypeScript types.
使用Zod作为运行时验证和TypeScript类型的唯一可信来源的Schema优先验证方案。

Quick Start

快速开始

typescript
import { z } from 'zod';

// 1. Define schema (validation + types in one place)
const schema = z.object({
  email: z.string().min(1, 'Required').email('Invalid email'),
  age: z.number().positive().optional()
});

// 2. Infer TypeScript types (never manually define)
type FormData = z.infer<typeof schema>;

// 3. Use with form library
import { zodResolver } from '@hookform/resolvers/zod';
const { register } = useForm<FormData>({
  resolver: zodResolver(schema)
});
typescript
import { z } from 'zod';

// 1. 定义Schema(验证规则与类型二合一)
const schema = z.object({
  email: z.string().min(1, '必填').email('无效邮箱'),
  age: z.number().positive().optional()
});

// 2. 推导TypeScript类型(无需手动定义)
type FormData = z.infer<typeof schema>;

// 3. 结合表单库使用
import { zodResolver } from '@hookform/resolvers/zod';
const { register } = useForm<FormData>({
  resolver: zodResolver(schema)
});

Core Principle: Reward Early, Punish Late

核心原则:早验证反馈正确、晚验证提示错误

This is the optimal validation timing pattern backed by UX research:
EventShow Valid (✓)Show Invalid (✗)Why
On input✅ Immediately❌ NeverDon't yell while typing
On blur✅ Immediately✅ YesUser finished, show errors
During correction✅ Immediately✅ Real-timeLet them fix quickly
这是经UX研究验证的最优验证时机模式:
事件显示正确状态(✓)显示错误状态(✗)原因
输入时✅ 立即显示❌ 绝不显示不要在用户输入时打断
失去焦点时✅ 立即显示✅ 显示错误用户已完成输入,展示错误
修正过程中✅ 立即显示✅ 实时更新让用户快速修正

Implementation

实现示例

typescript
// React Hook Form
useForm({
  mode: 'onBlur',           // First validation on blur (punish late)
  reValidateMode: 'onChange' // Re-validate on change (real-time correction)
});

// TanStack Form
useForm({
  validators: {
    onBlur: schema,          // Validate on blur
    onChange: schema         // Re-validate on change (after touched)
  }
});
typescript
// React Hook Form
useForm({
  mode: 'onBlur',           // 首次验证在失去焦点时(晚验证提示错误)
  reValidateMode: 'onChange' // 变更时重新验证(实时修正反馈)
});

// TanStack Form
useForm({
  validators: {
    onBlur: schema,          // 失去焦点时验证
    onChange: schema         // 变更时重新验证(标记为已触碰后)
  }
});

Zod Schema Patterns

Zod Schema 常用模式

Basic Types

基础类型

typescript
import { z } from 'zod';

// Strings
z.string()                          // Any string
z.string().min(1, 'Required')       // Non-empty (better than .nonempty())
z.string().email('Invalid email')
z.string().url('Invalid URL')
z.string().uuid('Invalid ID')
z.string().regex(/^\d{5}$/, 'Invalid ZIP')

// Numbers
z.number()                          // Any number
z.number().positive('Must be positive')
z.number().int('Must be whole number')
z.number().min(0).max(100)

// Booleans
z.boolean()
z.literal(true)                     // Must be exactly true

// Enums
z.enum(['admin', 'user', 'guest'])

// Arrays
z.array(z.string())
z.array(z.string()).min(1, 'Select at least one')

// Objects
z.object({
  name: z.string(),
  email: z.string().email()
})
typescript
import { z } from 'zod';

// 字符串类型
z.string()                          // 任意字符串
z.string().min(1, '必填')       // 非空(比.nonempty()更友好)
z.string().email('无效邮箱')
z.string().url('无效URL')
z.string().uuid('无效ID')
z.string().regex(/^\d{5}$/, '无效邮政编码')

// 数字类型
z.number()                          // 任意数字
z.number().positive('必须为正数')
z.number().int('必须为整数')
z.number().min(0).max(100)

// 布尔类型
z.boolean()
z.literal(true)                     // 必须严格为true

// 枚举类型
z.enum(['admin', 'user', 'guest'])

// 数组类型
z.array(z.string())
z.array(z.string()).min(1, '至少选择一项')

// 对象类型
z.object({
  name: z.string(),
  email: z.string().email()
})

Common Form Schemas

常见表单Schema

typescript
// schemas/auth.ts
export const loginSchema = z.object({
  email: z
    .string()
    .min(1, 'Please enter your email')
    .email('Please enter a valid email'),
  password: z
    .string()
    .min(1, 'Please enter your password'),
  rememberMe: z.boolean().optional().default(false)
});

export const registrationSchema = z.object({
  email: z
    .string()
    .min(1, 'Email is required')
    .email('Please enter a valid email'),
  password: z
    .string()
    .min(1, 'Password is required')
    .min(8, 'Password must be at least 8 characters')
    .regex(/[A-Z]/, 'Include at least one uppercase letter')
    .regex(/[a-z]/, 'Include at least one lowercase letter')
    .regex(/[0-9]/, 'Include at least one number'),
  confirmPassword: z
    .string()
    .min(1, 'Please confirm your password')
}).refine(data => data.password === data.confirmPassword, {
  message: 'Passwords do not match',
  path: ['confirmPassword']
});

export const forgotPasswordSchema = z.object({
  email: z
    .string()
    .min(1, 'Email is required')
    .email('Please enter a valid email')
});

export const resetPasswordSchema = z.object({
  password: z
    .string()
    .min(8, 'Password must be at least 8 characters'),
  confirmPassword: z.string()
}).refine(data => data.password === data.confirmPassword, {
  message: 'Passwords do not match',
  path: ['confirmPassword']
});
typescript
// schemas/profile.ts
export const profileSchema = z.object({
  firstName: z.string().min(1, 'First name is required'),
  lastName: z.string().min(1, 'Last name is required'),
  email: z.string().email('Invalid email'),
  phone: z
    .string()
    .regex(/^\+?[\d\s-()]+$/, 'Invalid phone number')
    .optional()
    .or(z.literal('')),
  bio: z
    .string()
    .max(500, 'Bio must be 500 characters or less')
    .optional()
});

export const addressSchema = z.object({
  street: z.string().min(1, 'Street address is required'),
  city: z.string().min(1, 'City is required'),
  state: z.string().min(1, 'State is required'),
  zip: z.string().regex(/^\d{5}(-\d{4})?$/, 'Invalid ZIP code'),
  country: z.string().min(1, 'Country is required').default('US')
});
typescript
// schemas/payment.ts
export const paymentSchema = z.object({
  cardName: z.string().min(1, 'Name on card is required'),
  cardNumber: z
    .string()
    .regex(/^\d{13,19}$/, 'Invalid card number')
    .refine(val => luhnCheck(val), 'Invalid card number'),
  expMonth: z
    .string()
    .regex(/^(0[1-9]|1[0-2])$/, 'Invalid month'),
  expYear: z
    .string()
    .regex(/^\d{2}$/, 'Invalid year')
    .refine(val => {
      const year = parseInt(val, 10) + 2000;
      return year >= new Date().getFullYear();
    }, 'Card has expired'),
  cvc: z.string().regex(/^\d{3,4}$/, 'Invalid CVC')
});

// Luhn algorithm for card validation
function luhnCheck(cardNumber: string): boolean {
  let sum = 0;
  let isEven = false;
  
  for (let i = cardNumber.length - 1; i >= 0; i--) {
    let digit = parseInt(cardNumber[i], 10);
    
    if (isEven) {
      digit *= 2;
      if (digit > 9) digit -= 9;
    }
    
    sum += digit;
    isEven = !isEven;
  }
  
  return sum % 10 === 0;
}
typescript
// schemas/auth.ts
export const loginSchema = z.object({
  email: z
    .string()
    .min(1, '请输入邮箱')
    .email('请输入有效的邮箱'),
  password: z
    .string()
    .min(1, '请输入密码'),
  rememberMe: z.boolean().optional().default(false)
});

export const registrationSchema = z.object({
  email: z
    .string()
    .min(1, '邮箱为必填项')
    .email('请输入有效的邮箱'),
  password: z
    .string()
    .min(1, '密码为必填项')
    .min(8, '密码长度至少为8位')
    .regex(/[A-Z]/, '至少包含一个大写字母')
    .regex(/[a-z]/, '至少包含一个小写字母')
    .regex(/[0-9]/, '至少包含一个数字'),
  confirmPassword: z
    .string()
    .min(1, '请确认密码')
}).refine(data => data.password === data.confirmPassword, {
  message: '两次输入的密码不一致',
  path: ['confirmPassword']
});

export const forgotPasswordSchema = z.object({
  email: z
    .string()
    .min(1, '邮箱为必填项')
    .email('请输入有效的邮箱')
});

export const resetPasswordSchema = z.object({
  password: z
    .string()
    .min(8, '密码长度至少为8位'),
  confirmPassword: z.string()
}).refine(data => data.password === data.confirmPassword, {
  message: '两次输入的密码不一致',
  path: ['confirmPassword']
});
typescript
// schemas/profile.ts
export const profileSchema = z.object({
  firstName: z.string().min(1, '名字为必填项'),
  lastName: z.string().min(1, '姓氏为必填项'),
  email: z.string().email('无效邮箱'),
  phone: z
    .string()
    .regex(/^\+?[\d\s-()]+$/, '无效电话号码')
    .optional()
    .or(z.literal('')),
  bio: z
    .string()
    .max(500, '个人简介不能超过500字符')
    .optional()
});

export const addressSchema = z.object({
  street: z.string().min(1, '街道地址为必填项'),
  city: z.string().min(1, '城市为必填项'),
  state: z.string().min(1, '州/省份为必填项'),
  zip: z.string().regex(/^\d{5}(-\d{4})?$/, '无效邮政编码'),
  country: z.string().min(1, '国家为必填项').default('US')
});
typescript
// schemas/payment.ts
export const paymentSchema = z.object({
  cardName: z.string().min(1, '持卡人姓名为必填项'),
  cardNumber: z
    .string()
    .regex(/^\d{13,19}$/, '无效卡号')
    .refine(val => luhnCheck(val), '无效卡号'),
  expMonth: z
    .string()
    .regex(/^(0[1-9]|1[0-2])$/, '无效月份'),
  expYear: z
    .string()
    .regex(/^\d{2}$/, '无效年份')
    .refine(val => {
      const year = parseInt(val, 10) + 2000;
      return year >= new Date().getFullYear();
    }, '卡片已过期'),
  cvc: z.string().regex(/^\d{3,4}$/, '无效CVC码')
});

// 用于卡号验证的Luhn算法
function luhnCheck(cardNumber: string): boolean {
  let sum = 0;
  let isEven = false;
  
  for (let i = cardNumber.length - 1; i >= 0; i--) {
    let digit = parseInt(cardNumber[i], 10);
    
    if (isEven) {
      digit *= 2;
      if (digit > 9) digit -= 9;
    }
    
    sum += digit;
    isEven = !isEven;
  }
  
  return sum % 10 === 0;
}

Advanced Patterns

进阶模式

Conditional Validation

条件验证

typescript
const orderSchema = z.object({
  deliveryMethod: z.enum(['shipping', 'pickup']),
  address: z.object({
    street: z.string(),
    city: z.string(),
    zip: z.string()
  }).optional()
}).refine(
  data => {
    if (data.deliveryMethod === 'shipping') {
      return data.address?.street && data.address?.city && data.address?.zip;
    }
    return true;
  },
  {
    message: 'Address is required for shipping',
    path: ['address']
  }
);
typescript
const orderSchema = z.object({
  deliveryMethod: z.enum(['shipping', 'pickup']),
  address: z.object({
    street: z.string(),
    city: z.string(),
    zip: z.string()
  }).optional()
}).refine(
  data => {
    if (data.deliveryMethod === 'shipping') {
      return data.address?.street && data.address?.city && data.address?.zip;
    }
    return true;
  },
  {
    message: '选择配送方式时必须填写地址',
    path: ['address']
  }
);

Cross-Field Validation

跨字段验证

typescript
const dateRangeSchema = z.object({
  startDate: z.date(),
  endDate: z.date()
}).refine(
  data => data.endDate >= data.startDate,
  {
    message: 'End date must be after start date',
    path: ['endDate']
  }
);
typescript
const dateRangeSchema = z.object({
  startDate: z.date(),
  endDate: z.date()
}).refine(
  data => data.endDate >= data.startDate,
  {
    message: '结束日期不能早于开始日期',
    path: ['endDate']
  }
);

Schema Composition

Schema 组合

typescript
// Base schemas
const nameSchema = z.object({
  firstName: z.string().min(1),
  lastName: z.string().min(1)
});

const contactSchema = z.object({
  email: z.string().email(),
  phone: z.string().optional()
});

// Composed schema
const userSchema = nameSchema.merge(contactSchema).extend({
  role: z.enum(['admin', 'user'])
});
typescript
// 基础Schema
const nameSchema = z.object({
  firstName: z.string().min(1),
  lastName: z.string().min(1)
});

const contactSchema = z.object({
  email: z.string().email(),
  phone: z.string().optional()
});

// 组合后的Schema
const userSchema = nameSchema.merge(contactSchema).extend({
  role: z.enum(['admin', 'user'])
});

Async Validation

异步验证

For server-side checks (username availability, email uniqueness):
typescript
// With Zod refine
const usernameSchema = z
  .string()
  .min(3, 'Username must be at least 3 characters')
  .refine(
    async (username) => {
      const response = await fetch(`/api/check-username?u=${encodeURIComponent(username)}`);
      const { available } = await response.json();
      return available;
    },
    { message: 'This username is already taken' }
  );

// With TanStack Form (built-in debouncing)
const form = useForm({
  defaultValues: { username: '' },
  validators: {
    onChangeAsyncDebounceMs: 500,
    onChangeAsync: async ({ value }) => {
      const response = await fetch(`/api/check-username?u=${value.username}`);
      const { available } = await response.json();
      if (!available) {
        return { fields: { username: 'Username is taken' } };
      }
      return undefined;
    }
  }
});
用于服务端校验(用户名可用性、邮箱唯一性等):
typescript
// 使用Zod refine
const usernameSchema = z
  .string()
  .min(3, '用户名长度至少为3位')
  .refine(
    async (username) => {
      const response = await fetch(`/api/check-username?u=${encodeURIComponent(username)}`);
      const { available } = await response.json();
      return available;
    },
    { message: '该用户名已被占用' }
  );

// 使用TanStack Form(内置防抖)
const form = useForm({
  defaultValues: { username: '' },
  validators: {
    onChangeAsyncDebounceMs: 500,
    onChangeAsync: async ({ value }) => {
      const response = await fetch(`/api/check-username?u=${value.username}`);
      const { available } = await response.json();
      if (!available) {
        return { fields: { username: '用户名已被占用' } };
      }
      return undefined;
    }
  }
});

Debounced Validation Helper

防抖验证工具

typescript
// utils/debounced-validator.ts
export function createDebouncedValidator<T>(
  validator: (value: T) => Promise<string | undefined>,
  delay: number = 500
) {
  let timeoutId: ReturnType<typeof setTimeout>;
  let latestValue: T;
  
  return (value: T): Promise<string | undefined> => {
    latestValue = value;
    
    return new Promise((resolve) => {
      clearTimeout(timeoutId);
      
      timeoutId = setTimeout(async () => {
        // Only validate if this is still the latest value
        if (value === latestValue) {
          const error = await validator(value);
          resolve(error);
        } else {
          resolve(undefined);
        }
      }, delay);
    });
  };
}

// Usage
const checkUsername = createDebouncedValidator(async (username: string) => {
  const response = await fetch(`/api/check-username?u=${username}`);
  const { available } = await response.json();
  return available ? undefined : 'Username is taken';
}, 500);
typescript
// utils/debounced-validator.ts
export function createDebouncedValidator<T>(
  validator: (value: T) => Promise<string | undefined>,
  delay: number = 500
) {
  let timeoutId: ReturnType<typeof setTimeout>;
  let latestValue: T;
  
  return (value: T): Promise<string | undefined> => {
    latestValue = value;
    
    return new Promise((resolve) => {
      clearTimeout(timeoutId);
      
      timeoutId = setTimeout(async () => {
        // 仅当当前值为最新值时才验证
        if (value === latestValue) {
          const error = await validator(value);
          resolve(error);
        } else {
          resolve(undefined);
        }
      }, delay);
    });
  };
}

// 使用示例
const checkUsername = createDebouncedValidator(async (username: string) => {
  const response = await fetch(`/api/check-username?u=${username}`);
  const { available } = await response.json();
  return available ? undefined : '用户名已被占用';
}, 500);

Error Messages

错误消息

Principles

设计原则

  1. Specific: Tell users exactly what's wrong
  2. Actionable: Tell users how to fix it
  3. Contextual: Reference the field name
  4. Friendly: Don't blame the user
  1. 具体明确: 准确告知用户问题所在
  2. 可操作: 告诉用户如何修正
  3. 上下文关联: 关联对应的字段名称
  4. 友好: 不要指责用户

Examples

示例对比

typescript
// ❌ BAD: Generic, unhelpful
const badSchema = z.object({
  email: z.string().email(),        // "Invalid"
  password: z.string().min(8),       // "Too short"
  phone: z.string().regex(/^\d+$/)   // "Invalid"
});

// ✅ GOOD: Specific, actionable
const goodSchema = z.object({
  email: z
    .string()
    .min(1, 'Please enter your email address')
    .email('Please enter a valid email (e.g., name@example.com)'),
  password: z
    .string()
    .min(1, 'Please create a password')
    .min(8, 'Password must be at least 8 characters'),
  phone: z
    .string()
    .regex(/^\d{10}$/, 'Please enter a 10-digit phone number')
});
typescript
// ❌ 不好:通用、无帮助
const badSchema = z.object({
  email: z.string().email(),        // "无效"
  password: z.string().min(8),       // "太短"
  phone: z.string().regex(/^\d+$/)   // "无效"
});

// ✅ 良好:具体、可操作
const goodSchema = z.object({
  email: z
    .string()
    .min(1, '请输入您的邮箱地址')
    .email('请输入有效的邮箱(例如:name@example.com)'),
  password: z
    .string()
    .min(1, '请设置密码')
    .min(8, '密码长度至少为8位'),
  phone: z
    .string()
    .regex(/^\d{10}$/, '请输入10位电话号码')
});

Message Templates

消息模板

typescript
// utils/validation-messages.ts
export const messages = {
  required: (field: string) => `Please enter your ${field}`,
  email: 'Please enter a valid email address',
  minLength: (field: string, min: number) => 
    `${field} must be at least ${min} characters`,
  maxLength: (field: string, max: number) => 
    `${field} must be ${max} characters or less`,
  pattern: (field: string, example: string) => 
    `Please enter a valid ${field} (e.g., ${example})`,
  match: (field: string) => `${field} fields must match`,
  unique: (field: string) => `This ${field} is already in use`,
  future: (field: string) => `${field} must be a future date`,
  past: (field: string) => `${field} must be a past date`
};

// Usage
const schema = z.object({
  email: z
    .string()
    .min(1, messages.required('email'))
    .email(messages.email),
  password: z
    .string()
    .min(1, messages.required('password'))
    .min(8, messages.minLength('Password', 8))
});
typescript
// utils/validation-messages.ts
export const messages = {
  required: (field: string) => `请输入您的${field}`,
  email: '请输入有效的邮箱地址',
  minLength: (field: string, min: number) => 
    `${field}长度至少为${min}`,
  maxLength: (field: string, max: number) => 
    `${field}长度不能超过${max}`,
  pattern: (field: string, example: string) => 
    `请输入有效的${field}(例如:${example}`,
  match: (field: string) => `两个${field}必须一致`,
  unique: (field: string) => `${field}已被使用`,
  future: (field: string) => `${field}必须是未来日期`,
  past: (field: string) => `${field}必须是过去日期`
};

// 使用示例
const schema = z.object({
  email: z
    .string()
    .min(1, messages.required('邮箱'))
    .email(messages.email),
  password: z
    .string()
    .min(1, messages.required('密码'))
    .min(8, messages.minLength('密码', 8))
});

Validation Timing Utility

验证时机工具

typescript
// utils/validation-timing.ts
export type ValidationMode = 'onBlur' | 'onChange' | 'onSubmit' | 'all';

export interface ValidationTimingConfig {
  /** When to first show errors */
  showErrorsOn: ValidationMode;
  /** When to re-validate after first error */
  revalidateOn: ValidationMode;
  /** Debounce delay for onChange (ms) */
  debounceMs?: number;
}

export const TIMING_PRESETS = {
  /** Default: Reward early, punish late */
  standard: {
    showErrorsOn: 'onBlur',
    revalidateOn: 'onChange'
  } as ValidationTimingConfig,
  
  /** For password strength, character counts */
  realtime: {
    showErrorsOn: 'onChange',
    revalidateOn: 'onChange'
  } as ValidationTimingConfig,
  
  /** For simple, short forms */
  submitOnly: {
    showErrorsOn: 'onSubmit',
    revalidateOn: 'onSubmit'
  } as ValidationTimingConfig,
  
  /** For expensive async validation */
  debounced: {
    showErrorsOn: 'onBlur',
    revalidateOn: 'onChange',
    debounceMs: 500
  } as ValidationTimingConfig
} as const;

// React Hook Form mapping
export function toRHFConfig(timing: ValidationTimingConfig) {
  return {
    mode: timing.showErrorsOn === 'all' ? 'all' : timing.showErrorsOn,
    reValidateMode: timing.revalidateOn === 'all' ? 'onChange' : timing.revalidateOn
  };
}
typescript
// utils/validation-timing.ts
export type ValidationMode = 'onBlur' | 'onChange' | 'onSubmit' | 'all';

export interface ValidationTimingConfig {
  /** 首次显示错误的时机 */
  showErrorsOn: ValidationMode;
  /** 首次错误后重新验证的时机 */
  revalidateOn: ValidationMode;
  /** onChange的防抖延迟(毫秒) */
  debounceMs?: number;
}

export const TIMING_PRESETS = {
  /** 默认:早验证反馈正确、晚验证提示错误 */
  standard: {
    showErrorsOn: 'onBlur',
    revalidateOn: 'onChange'
  } as ValidationTimingConfig,
  
  /** 适用于密码强度、字符计数场景 */
  realtime: {
    showErrorsOn: 'onChange',
    revalidateOn: 'onChange'
  } as ValidationTimingConfig,
  
  /** 适用于简单短表单 */
  submitOnly: {
    showErrorsOn: 'onSubmit',
    revalidateOn: 'onSubmit'
  } as ValidationTimingConfig,
  
  /** 适用于开销较大的异步验证 */
  debounced: {
    showErrorsOn: 'onBlur',
    revalidateOn: 'onChange',
    debounceMs: 500
  } as ValidationTimingConfig
} as const;

// React Hook Form 配置映射
export function toRHFConfig(timing: ValidationTimingConfig) {
  return {
    mode: timing.showErrorsOn === 'all' ? 'all' : timing.showErrorsOn,
    reValidateMode: timing.revalidateOn === 'all' ? 'onChange' : timing.revalidateOn
  };
}

File Structure

文件结构

form-validation/
├── SKILL.md
├── references/
│   ├── zod-patterns.md         # Deep-dive Zod patterns
│   ├── timing-research.md      # UX research on validation timing
│   └── error-message-guide.md  # Writing good error messages
└── scripts/
    ├── schemas/
    │   ├── auth.ts             # Login, registration, password reset
    │   ├── profile.ts          # User profile, addresses
    │   ├── payment.ts          # Credit cards, billing
    │   └── common.ts           # Reusable field schemas
    ├── validation-timing.ts    # Timing utilities
    ├── async-validator.ts      # Debounced async validation
    └── messages.ts             # Error message templates
form-validation/
├── SKILL.md
├── references/
│   ├── zod-patterns.md         # Zod模式深度解析
│   ├── timing-research.md      # 验证时机的UX研究
│   └── error-message-guide.md  # 错误消息编写指南
└── scripts/
    ├── schemas/
    │   ├── auth.ts             # 登录、注册、密码重置
    │   ├── profile.ts          # 用户资料、地址
    │   ├── payment.ts          # 信用卡、账单
    │   └── common.ts           # 可复用字段Schema
    ├── validation-timing.ts    # 验证时机工具
    ├── async-validator.ts      # 防抖异步验证工具
    └── messages.ts             # 错误消息模板

Framework Integration

框架集成

FrameworkAdapterImport
React Hook Form@hookform/resolvers/zod
zodResolver(schema)
TanStack Form@tanstack/zod-form-adapter
zodValidator()
VeeValidate@vee-validate/zod
toTypedSchema(schema)
VanillaDirect
schema.safeParse(data)
框架适配器导入方式
React Hook Form@hookform/resolvers/zod
zodResolver(schema)
TanStack Form@tanstack/zod-form-adapter
zodValidator()
VeeValidate@vee-validate/zod
toTypedSchema(schema)
Vanilla直接使用
schema.safeParse(data)

Reference

参考资料

  • references/zod-patterns.md
    — Complete Zod API patterns
  • references/timing-research.md
    — UX research backing timing decisions
  • references/error-message-guide.md
    — Writing effective error messages
  • references/zod-patterns.md
    — 完整Zod API模式
  • references/timing-research.md
    — 验证时机决策的UX研究依据
  • references/error-message-guide.md
    — 编写有效错误消息的指南