Hone logo
Hone
Problems

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:

  1. 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.
  2. A timing decorator: This decorator should measure and log the execution time of a method.
  3. 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 void or undefined.
  • 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 .ts file.
  • 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, and descriptor as arguments. The descriptor is crucial for modifying or replacing the method itself.
  • The descriptor.value property holds the actual function of the method. You will typically wrap this function with your decorator logic.
  • Use apply or call to invoke the original method within your decorator to preserve the correct this context.
  • 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.
Loading editor...
typescript