TypeScript Method Decorators: Complete Guide to Function Enhancement

Method decorators are a powerful TypeScript feature that allows you to modify or enhance class methods at runtime. This guide will show you how to leverage method decorators to write more maintainable and feature-rich TypeScript applications.

Table of Contents

What are Method Decorators?

Method decorators are special functions that can modify, observe, or replace class methods. They run when the method is defined, not when it’s called, allowing you to change or augment the method’s behavior during the class definition phase.

Basic Method Decorator Syntax

function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  // Store the original method
  const originalMethod = descriptor.value;

  // Replace the method with new functionality
  descriptor.value = function (...args: any[]) {
    console.log(`Calling method ${propertyKey} with args: ${args}`);
    return originalMethod.apply(this, args);
  };

  return descriptor;
}

class Calculator {
  @log
  add(a: number, b: number): number {
    return a + b;
  }
}
Code language: JavaScript (javascript)

Understanding Decorator Parameters

Method decorators receive three parameters:

  1. target: The prototype of the class (for instance methods) or the constructor function (for static methods)
  2. propertyKey: The name of the method being decorated
  3. descriptor: A PropertyDescriptor object that describes the method

Creating Your First Method Decorator

Let’s create a simple timing decorator that measures method execution time:

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

    descriptor.value = function (...args: any[]) {
      const start = performance.now();
      const result = originalMethod.apply(this, args);
      const end = performance.now();
      
      console.log(`${propertyKey} took ${end - start}ms to execute`);
      return result;
    };

    return descriptor;
  };
}

class DataProcessor {
  @timing()
  processData(data: number[]): number {
    return data.reduce((acc, curr) => acc + curr, 0);
  }
}
Code language: JavaScript (javascript)

Method Decorator Factory Pattern

Decorator factories allow you to customize decorator behavior by passing parameters:

function validate(validationFn: (value: any) => boolean) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
      if (!args.every(validationFn)) {
        throw new Error(`Invalid arguments for ${propertyKey}`);
      }
      return originalMethod.apply(this, args);
    };

    return descriptor;
  };
}

class NumberCalculator {
  @validate((n) => typeof n === 'number' && !isNaN(n))
  multiply(a: number, b: number): number {
    return a * b;
  }
}
Code language: JavaScript (javascript)

Practical Use Cases

1. Authentication Decorator

function requireAuth(roles: string[] = []) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
      const user = getCurrentUser(); // Implementation depends on your auth system
      
      if (!user) {
        throw new Error('User not authenticated');
      }

      if (roles.length && !roles.includes(user.role)) {
        throw new Error('Insufficient permissions');
      }

      return originalMethod.apply(this, args);
    };

    return descriptor;
  };
}

class UserService {
  @requireAuth(['admin'])
  deleteUser(userId: string): void {
    // Delete user logic
  }
}
Code language: JavaScript (javascript)

2. Caching Decorator

function memoize() {
  const cache = new Map();

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

    descriptor.value = function (...args: any[]) {
      const key = JSON.stringify(args);
      
      if (cache.has(key)) {
        return cache.get(key);
      }

      const result = originalMethod.apply(this, args);
      cache.set(key, result);
      return result;
    };

    return descriptor;
  };
}

class ExpensiveOperations {
  @memoize()
  fibonacci(n: number): number {
    if (n <= 1) return n;
    return this.fibonacci(n - 1) + this.fibonacci(n - 2);
  }
}
Code language: JavaScript (javascript)

Best Practices

  1. Keep Decorators Single-Purpose: Each decorator should focus on one specific functionality.


  2. Handle Errors Gracefully: Always include proper error handling in your decorators.


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

    descriptor.value = async function (...args: any[]) {
      try {
        return await originalMethod.apply(this, args);
      } catch (error) {
        console.error(`Error in ${propertyKey}:`, error);
        throw error;
      }
    };

    return descriptor;
  };
}
Code language: JavaScript (javascript)
  1. Maintain Type Safety: Use TypeScript’s type system to ensure decorator type safety.

Common Pitfalls and Solutions

Decorator Order

When using multiple decorators, remember they are applied from bottom to top:

class Example {
  @decoratorA
  @decoratorB
  method() {}
  // decoratorB is applied first, then decoratorA
}
Code language: JavaScript (javascript)

this Context

Be careful with the this context in decorators. Always use apply or call when invoking the original method:

descriptor.value = function (...args: any[]) {
  // Correct: Preserves 'this' context
  return originalMethod.apply(this, args);
  
  // Incorrect: Loses 'this' context
  return originalMethod(...args);
};
Code language: JavaScript (javascript)

Integration with Other TypeScript Features

Method decorators work well with other TypeScript features:

class ApiService {
  @log
  @errorBoundary()
  async fetchData<T>(url: string): Promise<T> {
    const response = await fetch(url);
    return response.json();
  }
}
Code language: JavaScript (javascript)

Testing Decorated Methods

When testing decorated methods, consider testing both the decorated and undecorated versions:

describe('Calculator', () => {
  it('should add numbers correctly with logging', () => {
    const calc = new Calculator();
    const result = calc.add(2, 3);
    expect(result).toBe(5);
  });

  it('should preserve original method behavior', () => {
    // Access original method if needed for testing
    const originalAdd = Calculator.prototype.add;
    expect(originalAdd.call(null, 2, 3)).toBe(5);
  });
});
Code language: PHP (php)

Method decorators are a powerful tool in TypeScript that can help you write more maintainable and feature-rich applications. By following these patterns and best practices, you can effectively use decorators to enhance your class methods while maintaining clean and maintainable code.

Remember to check TypeScript’s official documentation for updates on decorator syntax, as the feature is still evolving with newer versions of the language.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Share via
Copy link
Powered by Social Snap