Decorator Factories in TypeScript: A Complete Guide

Updated: January 7, 2024 By: Guest Contributor Post a comment

Introduction

Unpack the power of TypeScript decorators and streamline your coding process by mastering decorator factories, an advanced feature that enhances meta-programming capabilities.

What are Decorators?

Decorators are a design pattern that allows behavior to be added to an individual object, either statically or dynamically, without affecting the behavior of other objects from the same class. In TypeScript, decorators provide a way to add annotations and a meta-programming syntax for class declarations and members. Decorators are a stage 2 proposal for JavaScript and are available as an experimental feature of TypeScript.

Here is a simple decorator example:

function SimpleDecorator(constructor: Function) {
  console.log('SimpleDecorator called');
}

@SimpleDecorator
class MyClass {
  constructor() {
    console.log('MyClass created');
  }
}

What are Decorator Factories?

Decorator factories in TypeScript allow for customization of decorator functions. They are simply functions that return the expression that will be called by the decorator at runtime.

A basic decorator factory example:

function Color(value: string) {
  return function(target) {
    // This is the decorator
    Object.defineProperty(target, 'color', {
      value,
      writable: false
    });
  };
}

@Color('blue')
class ColoredClass {}

console.log((new ColoredClass() as any).color); // Outputs: 'blue'

Using Decorator Factories

Decorator factories can be used to customize how decorators are applied to classes, methods, accessors, properties, or parameters. By using decorator factories, you can pass parameters to your decorators and return different decorator functions depending on those parameters.

Class Decorators

function Component(selector: string) {
  return function (constructor: Function) {
    constructor.prototype.selector = selector;
  };
}

@Component('app-component')
class AppComponent {}

Method Decorators

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

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

  return propertyDesciptor;
}

Accessory Decorators

function Format(formatString: string) {
  return function(target: any, propertyName: string, descriptor: PropertyDescriptor) {
    let originalValue = descriptor.get;

    descriptor.get = function () {
      let result = originalValue.apply(this);
      return `Formatted date: ${result.toLocaleDateString()}`;
    };
  };
}

Property Decorators

function Default(defaultValue: any) {
  return function (target: any, propertyName: string) {
    let value = defaultValue;

    Object.defineProperty(target, propertyName, {
      get: () => value,
      set: (newValue) => { value = newValue; },
      enumerable: true,
      configurable: true
    });
  };
}

Parameter Decorators

function Print(target: Object, methodName: string, parameterIndex: number) {
  console.log(`Print decorator for ${methodName} called on parameter index ${parameterIndex}`);
}

Decorators with Dependencies

Sometimes decorators depend on other decorators, you can compose them to enhance their behavior. Here’s an example:

function First() {
  console.log('First Decorator Factory');
  return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log('First Decorator called');
  };
}

function Second() {
  console.log('Second Decorator Factory');
  return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log('Second Decorator called');
  };
}

class ExampleClass {
  @First()
  @Second()
  method() {}
}

In the above example, First will be executed before Second, even though it appears after Second in the code.

Conclusion

Decorator factories in TypeScript give tremendous power to add custom behavior to classes, methods, and more. They bolster the development process, allowing for a declarative and reusable code which is easier to read and maintain. As you continue to practice using decorators and decorator factories, you’ll discover new ways to optimize and enhance your TypeScript applications.