SOLUTION: Challenge 2 - Property Validator Decorator ===================================================== Challenge: Create a property decorator that enforces a constraint (e.g., email must be a valid format). Use it on a class property and test it. --- SOLUTION: function ValidateEmail(target: any, propertyKey: string) { let value: string; const getter = () => value; const setter = (newValue: string) => { // Simple email validation regex const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(newValue)) { throw new Error(`Invalid email: "${newValue}"`); } value = newValue; }; Object.defineProperty(target, propertyKey, { get: getter, set: setter, enumerable: true, configurable: true }); } // Test it class User { @ValidateEmail email: string = ""; constructor(email: string) { this.email = email; } } // Valid email const user1 = new User("alice@example.com"); console.log(user1.email); // "alice@example.com" ✅ // Invalid email try { const user2 = new User("not-an-email"); } catch (error) { console.error(error.message); // "Invalid email: "not-an-email"" ❌ } // Also fails if you modify after creation try { user1.email = "missing@domain"; // No TLD } catch (error) { console.error(error.message); } --- GENERALIZED: Validator with Custom Rules Make it reusable with a decorator factory: function Validate(isValid: (value: any) => boolean, message?: string) { return function(target: any, propertyKey: string) { let value: any; const getter = () => value; const setter = (newValue: any) => { if (!isValid(newValue)) { throw new Error(message || `Invalid value for ${propertyKey}: ${newValue}`); } value = newValue; }; Object.defineProperty(target, propertyKey, { get: getter, set: setter, enumerable: true, configurable: true }); }; } // Now you can reuse it for any property with custom validation: class Product { @Validate( (v) => typeof v === "string" && v.length > 0, "Name must be a non-empty string" ) name: string = ""; @Validate( (v) => typeof v === "number" && v > 0, "Price must be a positive number" ) price: number = 0; constructor(name: string, price: number) { this.name = name; this.price = price; } } const product = new Product("Laptop", 999); console.log(product.name, product.price); // "Laptop" 999 ✅ try { product.price = -100; } catch (error) { console.error(error.message); // "Price must be a positive number" ❌ } --- EXPLANATION: 1. PROPERTY DECORATOR SIGNATURE: (target: any, propertyKey: string) - target = the class prototype - propertyKey = the property name ("email", "name", "price", etc.) - NOTE: no descriptor parameter (unlike method decorators) 2. GETTER/SETTER: We replace the property with a getter and setter using Object.defineProperty. The setter is where validation happens. 3. CLOSURE: The `value` variable is captured in a closure between getter and setter. This keeps a private copy of the property value. 4. DECORATOR FACTORY: By wrapping the decorator in a function, you can accept parameters (rules, messages). This makes it reusable across different properties and classes. --- WHY THIS MATTERS: 1. Declarative validation: @Validate(...) is clearer than scattered if-checks. 2. Reusable: Define once, use on many properties. 3. Consistent: All validations follow the same pattern. 4. Framework-friendly: Angular/NestJS use this pattern extensively. --- CAUTION: Order of execution The property decorator runs when the class is defined, not when an instance is created. If you have multiple decorators on one property, they stack in order (bottom to top, like normal function composition).