/** * Validation rule function type. * Returns error message string if invalid, undefined/null if valid. */ export type ValidationRule = (value: T, formData: Record) => string | undefined | null /** * Field validation rules configuration. */ export interface FieldRules { required?: boolean | string minLength?: number | { value: number; message: string } maxLength?: number | { value: number; message: string } min?: number | { value: number; message: string } max?: number | { value: number; message: string } pattern?: RegExp | { value: RegExp; message: string } email?: boolean | string custom?: ValidationRule | ValidationRule[] } const validators = { required: (value: unknown, message = 'This field is required'): string | null => { if (value === null || value === undefined || value === '') { return message } if (Array.isArray(value) && value.length === 0) { return message } return null }, minLength: (value: unknown, min: number, message?: string): string | null => { if (typeof value !== 'string') return null if (value.length < min) { return message || `Must be at least ${min} characters` } return null }, maxLength: (value: unknown, max: number, message?: string): string | null => { if (typeof value !== 'string') return null if (value.length > max) { return message || `Must be at most ${max} characters` } return null }, min: (value: unknown, min: number, message?: string): string | null => { if (typeof value !== 'number') return null if (value < min) { return message || `Must be at least ${min}` } return null }, max: (value: unknown, max: number, message?: string): string | null => { if (typeof value !== 'number') return null if (value > max) { return message || `Must be at most ${max}` } return null }, pattern: (value: unknown, pattern: RegExp, message?: string): string | null => { if (typeof value !== 'string') return null if (!pattern.test(value)) { return message || 'Invalid format' } return null }, email: (value: unknown, message = 'Invalid email address'): string | null => { if (typeof value !== 'string' || !value) return null const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ if (!emailRegex.test(value)) { return message } return null }, } export function validateFieldValue( value: unknown, fieldRules: FieldRules | undefined, formData: Record, ): string | null { if (!fieldRules) return null if (fieldRules.required) { const message = typeof fieldRules.required === 'string' ? fieldRules.required : undefined const error = validators.required(value, message) if (error) return error } if (value === null || value === undefined || value === '') { return null } if (fieldRules.minLength !== undefined) { const config = typeof fieldRules.minLength === 'number' ? { value: fieldRules.minLength, message: undefined } : fieldRules.minLength const error = validators.minLength(value, config.value, config.message) if (error) return error } if (fieldRules.maxLength !== undefined) { const config = typeof fieldRules.maxLength === 'number' ? { value: fieldRules.maxLength, message: undefined } : fieldRules.maxLength const error = validators.maxLength(value, config.value, config.message) if (error) return error } if (fieldRules.min !== undefined) { const config = typeof fieldRules.min === 'number' ? { value: fieldRules.min, message: undefined } : fieldRules.min const error = validators.min(value, config.value, config.message) if (error) return error } if (fieldRules.max !== undefined) { const config = typeof fieldRules.max === 'number' ? { value: fieldRules.max, message: undefined } : fieldRules.max const error = validators.max(value, config.value, config.message) if (error) return error } if (fieldRules.pattern !== undefined) { const config = fieldRules.pattern instanceof RegExp ? { value: fieldRules.pattern, message: undefined } : fieldRules.pattern const error = validators.pattern(value, config.value, config.message) if (error) return error } if (fieldRules.email) { const message = typeof fieldRules.email === 'string' ? fieldRules.email : undefined const error = validators.email(value, message) if (error) return error } if (fieldRules.custom) { const customRules = Array.isArray(fieldRules.custom) ? fieldRules.custom : [fieldRules.custom] for (const rule of customRules) { const error = rule(value, formData) if (error) return error } } return null }