Mastering TypeScript Decorators: Advanced Patterns and Practical Use Cases

Explore advanced TypeScript decorators with practical examples. Learn how to enhance your code by mastering class, method, property, and parameter decorators.

TypeScript decorators are a powerful feature that allow you to modify classes, methods, properties, or parameters at design time. They provide a clean and reusable way to add extra logic to your code without cluttering the main business logic. In this tutorial, we'll explore advanced patterns and practical use cases of decorators to help you write better, cleaner, and more maintainable TypeScript code.

Before diving into advanced patterns, let's briefly cover what decorators are. A decorator is a special kind of declaration that can be attached to a class declaration, method, accessor, property, or parameter. The decorator is a function that receives details about the decorated element and can modify or replace it.

Let's start with a simple example of a class decorator that logs when an instance of a class is created:

typescript
function LogCreation(constructor: Function) {
  const original = constructor;

  function construct(constructor: any, args: any[]) {
    console.log(`Creating instance of ${constructor.name}`);
    return new constructor(...args);
  }

  const newConstructor: any = function (...args: any[]) {
    return construct(original, args);
  };

  newConstructor.prototype = original.prototype;
  return newConstructor;
}

@LogCreation
class Person {
  constructor(public name: string) {}
}

const p = new Person('Alice');

This decorator replaces the class constructor with a new one that logs a message before creating the instance.

### Advanced Pattern: Method Timing Decorator

Sometimes you want to measure how long a method takes to execute. Here's a method decorator to do exactly that:

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

  descriptor.value = function (...args: any[]) {
    console.time(`${propertyKey} execution time`);
    const result = originalMethod.apply(this, args);
    console.timeEnd(`${propertyKey} execution time`);
    return result;
  };

  return descriptor;
}

class Calculator {
  @Timer
  fibonacci(n: number): number {
    if (n <= 1) return n;
    return this.fibonacci(n - 1) + this.fibonacci(n - 2);
  }
}

const calc = new Calculator();
console.log(calc.fibonacci(10));

The `@Timer` decorator wraps the original method, measuring execution time using `console.time` and `console.timeEnd`. This pattern is useful for performance monitoring.

### Practical Use Case: Property Validation Decorator

To enforce constraints on class properties, a property decorator can validate inputs automatically.

typescript
function MinLength(length: number) {
  return function (target: any, propertyKey: string) {
    let value: string = '';

    const getter = () => value;
    const setter = (newVal: string) => {
      if (newVal.length < length) {
        throw new Error(
          `${propertyKey} should be at least ${length} characters long.`
        );
      }
      value = newVal;
    };

    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true,
    });
  };
}

class User {
  @MinLength(5)
  username: string;

  constructor(username: string) {
    this.username = username;
  }
}

const user = new User('admin'); // Works fine
// const badUser = new User('abc'); // Throws error

This pattern helps automate validation, keeping your code clean by avoiding manual checks in every setter or method.

### Parameter Decorator Example: Logging Parameter Index

Parameter decorators allow you to observe or modify parameter metadata. Here's a simple example that logs the index of the decorated parameter:

typescript
function LogParameter(
  target: any,
  propertyKey: string | symbol,
  parameterIndex: number
) {
  console.log(
    `Parameter at position ${parameterIndex} in method ${String(propertyKey)} is decorated.`
  );
}

class Greeter {
  greet(@LogParameter message: string) {
    console.log(message);
  }
}

const greeter = new Greeter();
greeter.greet('Hello!');

Each time the class is defined, the decorator logs the position of the parameter decorated. This is helpful for advanced metadata handling or frameworks.

### Summary

Decorators are an excellent tool to add reusable behavior like logging, validation, timing, or metadata collection without cluttering your core code. They make your classes more expressive and maintainable. With this tutorial, you've seen how to create and use advanced decorator patterns for classes, methods, properties, and parameters.

To get started with decorators in your project, make sure your `tsconfig.json` has `experimentalDecorators` enabled: { "compilerOptions": { "experimentalDecorators": true } } Experiment with these patterns and you'll quickly discover the power of TypeScript decorators!