Skip to Content
Course content
Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a PR from a junior dev who was trying to track down a performance bottleneck in our payment processing service. To find the slow spot, they had manually added console.time('methodName') at the start and console.timeEnd('methodName') at the end of about fifteen different methods. It worked, but the business logic was now buried under a mountain of boilerplate timing code. Every time they wanted to toggle the logging, they had to hunt through the whole file and comment out lines. It was a mess, and more importantly, it was fragile.

This is the exact problem method decorators were designed to solve. Instead of polluting your core logic with cross-cutting concerns—like logging, caching, or permission checks—you can "decorate" the method from the outside. It allows you to wrap the original function in a new one, giving you a hook to run code before and after the method executes without ever touching the method's internal body.

Intercepting the Execution Flow

When you write a method decorator, TypeScript gives you access to three things: the class prototype, the name of the method, and the PropertyDescriptor. The descriptor is the real prize here. It contains the value, which is the original function implementation. To modify the behavior, you save that original function to a variable and replace the descriptor's value with a new function.

function LogExecutionTime(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
        console.log(`--- Starting ${propertyKey} ---`);
        const start = performance.now();
        
        // We call the original method here
        const result = originalMethod.apply(this, args);
        
        const end = performance.now();
        console.log(`--- Finished ${propertyKey} in ${(end - start).toFixed(2)}ms ---`);
        
        return result;
    };

    return descriptor;
}

class PaymentService {
    @LogExecutionTime
    processTransaction(amount: number) {
        // Simulate a heavy API call
        return new Promise(resolve => setTimeout(() => resolve(`Processed $${amount}`), 500));
    }
}

In the example above, processTransaction doesn't even know it's being timed. The decorator intercepts the call, starts the timer, lets the original method do its work, and then logs the result. It's clean, reusable, and—most importantly—easy to remove if you no longer need it.

Preserving the 'this' Context

There is one major "gotcha" here that trips up almost everyone the first time: the this context. If you were to call originalMethod(...args) instead of originalMethod.apply(this, args), your code would likely crash the moment the method tried to access a class property. This is because the function is being detached from its instance.

By using .apply(this, args), you ensure that the original method still has access to the class instance it belongs to. I've seen developers try to use arrow functions for the wrapper, but be careful—arrow functions don't have their own this binding. For method decorators, a standard function declaration for the wrapper is usually your safest bet to keep the context intact.

One last thing: remember that decorators are still an experimental feature in TypeScript. You'll need "experimentalDecorators": true in your tsconfig.json for this to compile. It's a bit of a legacy holdover, but it's how the vast majority of the ecosystem (including frameworks like NestJS or Angular) handles this logic.




📋 Practical Task

Exercise: Building a Method-Level Permission Guard

You are building a banking application. Some methods in your AccountService should only be accessible to users with an 'admin' role. Instead of writing if (user.role !== 'admin') at the start of every single sensitive method, you're going to build a decorator to handle this.

Requirements:

  • Create a @RequiredRole(role: string) decorator factory (a function that returns a decorator).
  • The decorator should check a property called currentUserRole on the class instance.
  • If the currentUserRole matches the required role, the method should execute normally.
  • If the roles do not match, the method should throw an Error: "Unauthorized: Required role [role] not met."
  • Implement an AccountService class with a currentUserRole property and a method deleteAccount(id: string) decorated with @RequiredRole('admin').

Test your implementation: Set the currentUserRole to 'user' and attempt to call deleteAccount() to ensure it throws the error. Then change it to 'admin' and verify it succeeds.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.