← 返回导航

表单校验规则类型

业务类型 · 实战

场景说明

前端表单开发中,校验规则配置是高频场景:每个字段可能有必填、正则、最小值、最大值、自定义函数等多种校验方式。用泛型约束校验规则的字段名必须是表单数据的 key,可以在编译期就捕获字段名拼写错误,避免运行时才发现校验不生效。同时,校验规则本身也需要类型化,使规则的类型与字段值类型匹配,比如数字字段的 min/max 天然是 number,字符串字段的 pattern 天然是 RegExp。

完整代码


      
// 表单校验规则类型:字段名泛型约束 + 规则类型与值类型匹配
// 用 keyof 约束字段名,避免拼写错误在编译期暴露

// 1. 单条校验规则
interface ValidationRule<T = any> {
  required?: boolean;           // 是否必填
  message?: string;             // 校验失败提示
  pattern?: RegExp;            // 正则校验
  min?: number;               // 最小值 / 最小长度
  max?: number;               // 最大值 / 最大长度
  validator?: (value: T) => boolean | string | Promise<boolean | string>;  // 自定义校验
}

// 2. 校验规则集合:字段名必须是表单数据的 key
type ValidationRules<T extends Record<string, any>> = {
  [K in keyof T]?: ValidationRule<T[K]>;
};

// 3. 校验结果
interface FieldError {
  valid: boolean;
  message?: string;
}

type ValidationResult<T> = {
  [K in keyof T]?: FieldError;
};

// 4. 表单数据示例
interface LoginForm {
  username: string;
  password: string;
  age: number;
  email: string;
}

// 5. 定义校验规则:字段名受 LoginForm 的 key 约束
const rules: ValidationRules<LoginForm> = {
  username: {
    required: true,
    min: 3,
    max: 20,
    message: '用户名长度为 3-20 位',
  },
  password: {
    required: true,
    min: 6,
    pattern: /^[a-zA-Z0-9_]+$/,
    message: '密码至少 6 位,仅含字母数字下划线',
  },
  age: {
    required: false,
    min: 0,
    max: 120,
    validator: (v) => v >= 0 || '年龄必须大于 0',
  },
  email: {
    required: true,
    pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
    message: '请输入有效的邮箱地址',
  },
};

// 6. 校验执行器函数
function validateForm<T extends Record<string, any>>(
  form: T,
  rules: ValidationRules<T>,
): ValidationResult<T> {
  const result: ValidationResult<T> = {};
  for (const key in rules) {
    const rule = rules[key];
    const value = form[key];
    if (!rule) continue;

    if (rule.required && (value === null || value === undefined || value === '')) {
      result[key] = { valid: false, message: rule.message || '该字段不能为空' };
      continue;
    }
    if (rule.pattern && typeof value === 'string' && !rule.pattern.test(value)) {
      result[key] = { valid: false, message: rule.message || '格式不正确' };
      continue;
    }
    if (rule.min !== undefined && typeof value === 'number' && value < rule.min) {
      result[key] = { valid: false, message: rule.message || `最小值为 ${rule.min}` };
      continue;
    }
    if (rule.max !== undefined && typeof value === 'number' && value > rule.max) {
      result[key] = { valid: false, message: rule.message || `最大值为 ${rule.max}` };
      continue;
    }
    result[key] = { valid: true };
  }
  return result;
}

// 使用
const form: LoginForm = { username: 'a', password: '123', age: 25, email: 'a@b.com' };
const result = validateForm(form, rules);

关键点解析

使用示例与类型推导


      
// 字段名拼写错误会被 TS 拦截
const badRules: ValidationRules<LoginForm> = {
  // usrname: { required: true }  // 错误:'usrname' 不存在于 LoginForm
};

// 校验结果类型推导
type R = ValidationResult<LoginForm>;
// => { username?: FieldError; password?: FieldError; age?: FieldError; email?: FieldError }

// 自定义校验函数参数类型自动推导
const ageRule: ValidationRule<number> = {
  validator: (v) => {
    // v 是 number 类型
    return v > 0 && v < 150;
  }
};

// 新表单复用同一套校验类型
interface RegisterForm {
  phone: string;
  confirmPassword: string;
}
const registerRules: ValidationRules<RegisterForm> = {
  phone: { required: true, pattern: /^1\d{10}$/ },
};
在 Playground 中尝试