Mastering Method Decorators in TypeScript
This challenge focuses on understanding and implementing method decorators in TypeScript. You will build a system that allows you to augment the behavior of existing class methods without modifying their original source code. This is a powerful technique for adding cross-cutting concerns like logging, timing, or authorization.
Problem Description
Your task is to create a set of reusable TypeScript method decorators. You will then apply these decorators to a sample class to demonstrate their functionality. Specifically, you need to implement:
- A logging decorator: This decorator should log the method name and its arguments before the method is executed, and log the return value (or an indication of an error) after execution.
- A timing decorator: This decorator should measure and log the execution time of a method.
- A decorator factory that accepts arguments: Create a decorator that can be configured with an argument. For instance, a decorator that checks if a user has a specific permission before allowing a method to execute.
Key Requirements:
- All decorators must be implemented as method decorators in TypeScript.
- Decorators should be pure functions that do not mutate the original method's behavior beyond what they are designed to do (e.g., logging, timing).
- The logging decorator should handle both successful returns and thrown errors gracefully.
- The timing decorator should accurately measure execution time.
- The decorator factory should demonstrate passing configuration to the decorator.
Expected Behavior:
When decorators are applied to a class's methods and the class instances are used, the decorated methods should exhibit the augmented behavior (logging, timing, conditional execution) in addition to their original functionality.
Edge Cases to Consider:
- Methods with no arguments.
- Methods that return
voidorundefined. - Methods that throw exceptions.
- The order of multiple decorators applied to the same method.
Examples
Example 1: Logging Decorator
Consider a simple class Calculator with a add method.
class Calculator {
@LogMethod
add(a: number, b: number): number {
console.log(`Executing add(${a}, ${b})...`); // Original method logic
return a + b;
}
}
const calc = new Calculator();
const sum = calc.add(5, 3);
console.log(`Result: ${sum}`);
Expected Output (similar to):
[LogMethod] Entering method: add
[LogMethod] Arguments: [5, 3]
Executing add(5, 3)...
[LogMethod] Method 'add' returned: 8
Result: 8
Example 2: Timing Decorator
Now, let's apply the LogExecutionTime decorator to the same add method.
class Calculator {
@LogExecutionTime
add(a: number, b: number): number {
// Simulate some work
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += i;
}
return a + b + result; // Original method logic
}
}
const calc = new Calculator();
const sum = calc.add(5, 3);
console.log(`Final sum: ${sum}`);
Expected Output (similar to):
[LogExecutionTime] Entering method: add
[LogExecutionTime] Method 'add' took X.XXX ms to execute.
Final sum: [some large number]
(Note: X.XXX will vary based on execution environment and system load)
Example 3: Decorator Factory with Argument
Let's imagine an AuthenticationService and a UserService with a getUserProfile method. We want to protect this method, allowing access only if a user has the 'admin' role.
interface User {
id: number;
username: string;
roles: string[];
}
// Mock Authentication Service
class AuthService {
getCurrentUser(): User | null {
// In a real app, this would fetch the logged-in user
return { id: 1, username: 'alice', roles: ['user', 'admin'] };
}
}
// Decorator Factory
function RequireRole(role: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const authService = new AuthService(); // Mock instantiation
const currentUser = authService.getCurrentUser();
if (currentUser && currentUser.roles.includes(role)) {
console.log(`[RequireRole] User '${currentUser.username}' has role '${role}'. Proceeding.`);
return originalMethod.apply(this, args);
} else {
console.error(`[RequireRole] Access denied. User does not have the required role '${role}'.`);
throw new Error(`Unauthorized: Required role '${role}' not found.`);
}
};
return descriptor;
};
}
class UserService {
@RequireRole('admin')
getAdminDashboardData(): string {
console.log("Fetching admin dashboard data...");
return "Sensitive admin data.";
}
@RequireRole('user')
getUserProfile(userId: number): string {
console.log(`Fetching profile for user ${userId}...`);
return `Profile for user ${userId}`;
}
}
const userService = new UserService();
try {
const adminData = userService.getAdminDashboardData();
console.log(adminData);
} catch (error: any) {
console.error(error.message);
}
try {
const userProfile = userService.getUserProfile(123);
console.log(userProfile);
} catch (error: any) {
console.error(error.message);
}
Expected Output (similar to):
[RequireRole] User 'alice' has role 'admin'. Proceeding.
Fetching admin dashboard data...
Sensitive admin data.
[RequireRole] User 'alice' has role 'user'. Proceeding.
Fetching profile for user 123...
Profile for user 123
Constraints
- TypeScript version: 4.0 or higher (for decorator support).
- The provided solution should be in a single
.tsfile. - Decorators should be implemented using the standard TypeScript decorator syntax.
- Avoid using external libraries for decorator implementation. You should build them from scratch.
- The focus is on understanding the mechanism of method decorators and their application.
Notes
- Remember that method decorators receive
target,propertyKey, anddescriptoras arguments. Thedescriptoris crucial for modifying or replacing the method itself. - The
descriptor.valueproperty holds the actual function of the method. You will typically wrap this function with your decorator logic. - Use
applyorcallto invoke the original method within your decorator to preserve the correctthiscontext. - Consider how multiple decorators are applied. They are executed in reverse order of their declaration.
- This challenge is designed to deepen your understanding of metaprogramming in TypeScript. Experiment with different scenarios and decorator combinations.