前端表单开发中,校验规则配置是高频场景:每个字段可能有必填、正则、最小值、最大值、自定义函数等多种校验方式。用泛型约束校验规则的字段名必须是表单数据的 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);
ValidationRules<T>:用 [K in keyof T] 遍历表单数据的 key,规则对象的键必须是 T 的属性,写错字段名 TS 会直接报错,把运行时问题提前到编译期。ValidationRule<T[K]>:每条规则的泛型参数是对应字段的值类型,自定义 validator 的入参自动推导为字段类型,类型安全有保障。ValidationResult<T>:校验结果的键与表单数据一一对应,每个字段对应一个 FieldError,访问结果时享有完整的类型提示。// 字段名拼写错误会被 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}$/ },
};