SOLUTION: Challenge 2 - Validation with Error Collection ========================================================= Challenge: Write a validator that collects all errors instead of throwing on first. Return an array of errors or empty array on success. --- SOLUTION: class ValidationError { constructor( readonly field: string, readonly message: string, readonly value?: unknown ) {} toString(): string { return `${this.field}: ${this.message}`; } } interface User { name: string; email: string; age: number; password: string; } // Collect-all validator function validateUser(data: unknown): ValidationError[] { const errors: ValidationError[] = []; // Type check first if (typeof data !== "object" || data === null) { return [new ValidationError("_root", "Data must be an object")]; } const user = data as Record; // Validate name if (!user.name) { errors.push(new ValidationError("name", "Name is required")); } else if (typeof user.name !== "string") { errors.push(new ValidationError("name", "Name must be a string", user.name)); } else if (user.name.length < 2) { errors.push(new ValidationError("name", "Name must be at least 2 characters", user.name)); } // Validate email if (!user.email) { errors.push(new ValidationError("email", "Email is required")); } else if (typeof user.email !== "string") { errors.push(new ValidationError("email", "Email must be a string", user.email)); } else if (!user.email.includes("@")) { errors.push(new ValidationError("email", "Email must contain @", user.email)); } // Validate age if (user.age === undefined) { errors.push(new ValidationError("age", "Age is required")); } else if (typeof user.age !== "number") { errors.push(new ValidationError("age", "Age must be a number", user.age)); } else if (user.age < 18) { errors.push(new ValidationError("age", "Must be at least 18", user.age)); } else if (user.age > 120) { errors.push(new ValidationError("age", "Age seems unrealistic", user.age)); } // Validate password if (!user.password) { errors.push(new ValidationError("password", "Password is required")); } else if (typeof user.password !== "string") { errors.push(new ValidationError("password", "Password must be a string", user.password)); } else if (user.password.length < 8) { errors.push(new ValidationError("password", "Password must be at least 8 characters")); } else if (!/[A-Z]/.test(user.password)) { errors.push(new ValidationError("password", "Password must contain uppercase letter")); } else if (!/[0-9]/.test(user.password)) { errors.push(new ValidationError("password", "Password must contain number")); } return errors; } --- // Usage console.log("=== Test Case 1: Valid User ==="); const validUser = { name: "Alice", email: "alice@example.com", age: 25, password: "SecurePass123" }; const errors1 = validateUser(validUser); if (errors1.length === 0) { console.log("✅ Validation passed!"); } else { console.log("❌ Validation failed:"); errors1.forEach(e => console.log(` ${e.toString()}`)); } console.log("\n=== Test Case 2: Multiple Errors ==="); const invalidUser = { name: "B", email: "not-an-email", age: 15, password: "weak" }; const errors2 = validateUser(invalidUser); if (errors2.length === 0) { console.log("✅ Validation passed!"); } else { console.log("❌ Validation failed:"); errors2.forEach(e => console.log(` ${e.toString()}`)); } console.log("\n=== Test Case 3: Missing Fields ==="); const emptyUser = {}; const errors3 = validateUser(emptyUser); if (errors3.length === 0) { console.log("✅ Validation passed!"); } else { console.log("❌ Validation failed:"); errors3.forEach(e => console.log(` ${e.toString()}`)); } --- ADVANCED: Reusable Validation Rules // Define rules separately for reusability const validationRules = { name: [ { check: (v: unknown) => v !== undefined && v !== null && v !== "", message: "Name is required" }, { check: (v: unknown) => typeof v === "string", message: "Name must be a string" }, { check: (v: unknown) => typeof v === "string" && v.length >= 2, message: "Name must be at least 2 characters" } ], email: [ { check: (v: unknown) => v !== undefined && v !== null && v !== "", message: "Email is required" }, { check: (v: unknown) => typeof v === "string" && v.includes("@"), message: "Email must be valid" } ] }; function validateWithRules( data: Record, rules: Record boolean; message: string }>> ): ValidationError[] { const errors: ValidationError[] = []; for (const [field, fieldRules] of Object.entries(rules)) { const value = data[field]; for (const rule of fieldRules) { if (!rule.check(value)) { errors.push(new ValidationError(field, rule.message, value)); break; // Stop checking this field after first failure } } } return errors; } // Usage const errors = validateWithRules(invalidUser, validationRules); --- EXPLANATION: COLLECT-ALL STRATEGY: Instead of: if (!name) throw new Error("Name required"); if (!email) throw new Error("Email required"); // Never reaches here if first throws Do: const errors = []; if (!name) errors.push(...); if (!email) errors.push(...); return errors; USER FEEDBACK: User sees all validation errors at once: ✅ "Name must be at least 2 characters" ✅ "Email must contain @" ✅ "Age must be at least 18" Not: ❌ "Name must be at least 2 characters" (then fix, submit again, get next error) ❌ "Email must contain @" (then fix, submit again) ❌ etc. WHEN TO USE: Collect-all: user-facing forms (UX matters) Fail-fast: internal validation (performance matters) --- TYPE-SAFE VALIDATION: You could also type the result: type ValidationResult = | { ok: true; data: T } | { ok: false; errors: ValidationError[] }; function validateUser(data: unknown): ValidationResult { const errors = validateUser(data); if (errors.length > 0) { return { ok: false, errors }; } return { ok: true, data: data as User }; } This forces callers to handle both success and failure cases. --- REAL-WORLD PATTERN: In Express/NestJS: app.post("/users", (req, res) => { const errors = validateUser(req.body); if (errors.length > 0) { return res.status(400).json({ status: "error", errors: errors.map(e => ({ field: e.field, message: e.message })) }); } // Proceed with creating user res.status(201).json({ status: "success", data: user }); }); This gives clients a structured error response to show to the user.