validateFields with per-field error maps
TypeScript
export interface FieldRule {
required?: boolean;
min?: number;
max?: number;
pattern?: RegExp;
label?: string;
}
/** Validate a flat payload against declarative rules and collect field errors. */
export function validateFields(
input: Record<string, unknown>,
rules: Record<string, FieldRule>,
): { valid: boolean; errors: Record<string, string[]> } {
const errors: Record<string, string[]> = {};
const add = (field: string, message: string): void => {
(errors[field] ??= []).push(message);
};
for (const [field, rule] of Object.entries(rules)) {
const label = rule.label ?? field;
const value = input[field];
if (value === undefined || value === null || value === '') {
if (rule.required) add(field, `${label} is required`);
continue;
}
if (typeof value === 'string') {
if (rule.min !== undefined && value.length < rule.min) add(field, `${label} must have at least ${rule.min} characters`);
if (rule.max !== undefined && value.length > rule.max) add(field, `${label} must have at most ${rule.max} characters`);
if (rule.pattern && !rule.pattern.test(value)) add(field, `${label} has an invalid format`);
} else if (typeof value === 'number') {
if (rule.min !== undefined && value < rule.min) add(field, `${label} must be >= ${rule.min}`);
if (rule.max !== undefined && value > rule.max) add(field, `${label} must be <= ${rule.max}`);
} else {
add(field, `${label} must be a string or number`);
}
}
return { valid: Object.keys(errors).length === 0, errors };
}