Decorators provide a way to add annotations and metadata to classes, methods, and properties.

Enabling Decorators

Enable in tsconfig.json:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Class Decorators

Applied to class constructors.

function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

@sealed
class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
}

Decorator Factory

Returns a decorator function.

function color(value: string) {
  return function (constructor: Function) {
    // Do something with constructor and value
  };
}

@color("red")
class Car {}

Method Decorators

Applied to method descriptors.

function enumerable(value: boolean) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    descriptor.enumerable = value;
  };
}

class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }

  @enumerable(false)
  greet() {
    return "Hello, " + this.greeting;
  }
}

Property Decorators

Applied to class properties.

function format(formatString: string) {
  return function (target: any, propertyKey: string) {
    // Store metadata about the property
  };
}

class User {
  @format("uppercase")
  name: string;
}

Parameter Decorators

Applied to function parameters.

function required(target: any, propertyKey: string, parameterIndex: number) {
  // Mark parameter as required
}

class UserService {
  createUser(@required name: string, age: number) {
    // ...
  }
}

Accessor Decorators

Applied to getters and setters.

function configurable(value: boolean) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    descriptor.configurable = value;
  };
}

class Point {
  private _x: number;
  private _y: number;

  @configurable(false)
  get x() {
    return this._x;
  }
}

Common Decorator Patterns

Logging Decorator

function log(target: any, propertyName: string, descriptor: PropertyDescriptor) {
  const method = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyName} with`, args);
    const result = method.apply(this, args);
    console.log(`Result:`, result);
    return result;
  };
}

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

Validation Decorator

function validate(min: number, max: number) {
  return function (target: any, propertyKey: string) {
    let value: number;

    const getter = () => value;
    const setter = (newVal: number) => {
      if (newVal < min || newVal > max) {
        throw new Error(`Value must be between ${min} and ${max}`);
      }
      value = newVal;
    };

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

class Product {
  @validate(0, 100)
  price: number;
}

Decorator Execution Order

Decorators are applied in a specific order:

  1. Parameter decorators
  2. Method/accessor decorators
  3. Property decorators
  4. Class decorators

Exercises

Exercise 1: Class Decorator

// Create a decorator 'timing' that:
// - Measures how long a method takes to execute
// - Logs the duration
// - Apply it to a class method

Exercise 2: Method Decorator

// Create a decorator 'deprecated' that:
// - Logs a warning when a deprecated method is called
// - Apply it to an old method

Exercise 3: Property Decorator

// Create a decorator 'readonly' that:
// - Makes a property readonly after initialization
// - Apply it to a class property

Exercise 4: Parameter Decorator

// Create a decorator 'validate' that:
// - Validates a parameter before the function runs
// - Throws an error if validation fails

Exercise 5: Decorator Factory

// Create a decorator factory 'cache' that:
// - Caches method results
// - Takes a TTL (time to live) parameter
// - Returns cached result if available and not expired

Best Practices

  1. Use decorators sparingly - They can make code harder to understand
  2. Document decorator behavior - Add comments explaining what they do
  3. Consider alternatives - Sometimes functions or mixins are clearer
  4. Test decorators - Ensure they work as expected
  5. Be aware of execution order - Decorators execute in a specific order

Common Mistakes

  1. Forgetting to enable decorators
// ❌ Missing in tsconfig.json
{
  "compilerOptions": {
    // Need experimentalDecorators: true
  }
}
  1. Wrong decorator signature
// ❌ Wrong parameters
function myDecorator(target: any) {
  // Method decorator needs 3 parameters
}

// ✅ Correct
function myDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  // ...
}
  1. Not returning descriptor
// ❌ Forgetting to return
function myDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  descriptor.value = function() { /* ... */ };
  // Missing return descriptor;
}

// ✅ Correct
function myDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  descriptor.value = function() { /* ... */ };
  return descriptor;
}

Next Steps

Learn about Type Guards.