SOLUTION: Challenge 1 - Method Logging Decorator ================================================== Challenge: Create a method decorator that logs when a method is called and what it returns. Apply it to a sample class method. --- SOLUTION: function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; descriptor.value = function(...args: any[]) { console.log(`[${propertyKey}] Called with args:`, args); const result = originalMethod.apply(this, args); console.log(`[${propertyKey}] Returned:`, result); return result; }; return descriptor; } // Test it class Calculator { @Log add(a: number, b: number): number { return a + b; } @Log multiply(a: number, b: number): number { return a * b; } } const calc = new Calculator(); calc.add(5, 3); // Output: // [add] Called with args: [ 5, 3 ] // [add] Returned: 8 calc.multiply(4, 7); // Output: // [multiply] Called with args: [ 4, 7 ] // [multiply] Returned: 28 --- EXPLANATION: 1. SIGNATURE: (target: any, propertyKey: string, descriptor: PropertyDescriptor) - target = the class prototype - propertyKey = name of the method ("add", "multiply", etc.) - descriptor = property descriptor object (contains .value which is the original function) 2. DESCRIPTOR.VALUE: This holds the original method function. We save it, then replace descriptor.value with a wrapper that: - Logs the method name and arguments - Calls the original method with apply() - Logs the result - Returns it 3. CONTEXT (this): Important: use .apply(this, args) to preserve the correct 'this' binding. If you used originalMethod(...args), 'this' would be undefined inside the method. 4. RETURN: Always return the descriptor at the end. TypeScript uses it to finalize the change. --- ADVANCED: Async method support If your method returns a Promise, you need to handle that: function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; descriptor.value = async function(...args: any[]) { console.log(`[${propertyKey}] Called with args:`, args); const result = await originalMethod.apply(this, args); console.log(`[${propertyKey}] Returned:`, result); return result; }; return descriptor; } // Now works with async methods too class AsyncCalculator { @Log async fetchSum(a: number, b: number): Promise { await new Promise(resolve => setTimeout(resolve, 100)); return a + b; } } --- WHY THIS MATTERS: 1. Cross-cutting concerns: Logging, timing, caching, auth — all can be decorators. 2. DRY: Write the decorator once, apply it to many methods. 3. Non-invasive: The original method logic is unchanged; the decorator wraps it. 4. Composable: Stack multiple decorators on the same method.