Decorators are a powerful feature in TypeScript that allow you to add functionality to a class, method, or property. They are essentially functions that can modify the behavior of the code they are applied to, and can be used for a variety of purposes such as logging, validation, and more. In this tutorial, we will explore decorators in TypeScript and see how they can be used to enhance your code.


Introduction to Decorators

A decorator is a special kind of declaration that can be attached to a class declaration, method, accessor, property, or parameter. Decorators use the form @expression, where expression must evaluate to a function that will be called at runtime with information about the decorated declaration. The decorator function can then modify the behavior of the declaration or even replace it with a new definition.

In TypeScript, decorators are applied using the @ symbol followed by the decorator function name. For example, let's say we have a class Person and we want to add a decorator called log to it:

function log(target: Function) {
  console.log(`Logging ${target.name}...`);
}

@log
class Person {
  constructor(public name: string, public age: number) {}
}

In this example, the log decorator function takes a single parameter called target, which is a reference to the decorated class. The decorator function simply logs a message indicating that the class is being logged. The @log syntax is used to apply the decorator to the Person class.

When we run the code, we should see the following output:

Logging Person...

This demonstrates that the decorator function was called when the class was defined.


Class Decorators

Class decorators are applied to the entire class declaration and can be used to modify its behavior or add new functionality. A class decorator is a function that takes a single parameter, which is a reference to the decorated class.

Let's create an example of a class decorator that adds a debug method to the class:

function debug(target: Function) {
  target.prototype.debug = function() {
    console.log(`DEBUG: ${JSON.stringify(this)}`);
  }
}

@debug
class Person {
  constructor(public name: string, public age: number) {}
}

const person = new Person('John', 30);
person.debug();

In this example, the debug decorator function adds a debug method to the prototype of the Person class. The method simply logs the instance of the class as a JSON string.

When we create a new instance of the Person class and call the debug method, we should see the following output:

DEBUG: {"name":"John","age":30}

This demonstrates that the decorator function successfully added the debug method to the class.


Method Decorators

Method decorators are applied to the methods of a class and can be used to modify their behavior or add new functionality. A method decorator is a function that takes three parameters: the target object, the name of the method being decorated, and a property descriptor that describes the method.

Let's create an example of a method decorator that logs the arguments and return value of a method:

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

  descriptor.value = function (...args: any[]) {
    const result = originalMethod.apply(this, args);
    console.log(`Method ${key} called with args ${JSON.stringify(args)}, result: ${JSON.stringify(result)}`);
    return result;
  };

  return descriptor;
}

class Calculator {
  @logMethod
  sum(a: number, b: number):
{
return a + b;
}
}

const calculator = new Calculator();
const result = calculator.sum(2, 3);
console.log(result);

In this example, the `logMethod` decorator function takes three parameters: `target`, `key`, and `descriptor`. The `target` parameter is a reference to the object being decorated (in this case, the `Calculator` class), `key` is the name of the method being decorated (`sum`), and `descriptor` is an object that describes the method.

The decorator function first saves a reference to the original method using the `originalMethod` variable. It then replaces the method with a new function that calls the original method, logs its arguments and return value, and returns the result.

When we create a new instance of the `Calculator` class and call the `sum` method, we should see the following output:

Method sum called with args [2,3], result: 5

5

This demonstrates that the decorator function successfully modified the behavior of the `sum` method.


Property Decorators

Property decorators are applied to the properties of a class and can be used to modify their behavior or add new functionality. A property decorator is a function that takes two parameters: the target object and the name of the property being decorated.

Let's create an example of a property decorator that adds a validation check to a property:

function validate(target: any, key: string) {
  let value = target[key];

  const getter = function() {
    return value;
  };

  const setter = function(newValue: any) {
    if (typeof newValue !== 'string') {
      throw new Error('Property must be a string');
    }

    value = newValue;
  };

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

class Person {
  @validate
  name: string;

  constructor(name: string, public age: number) {
    this.name = name;
  }
}

const person = new Person('John', 30);
person.name = '123'; // throws an error

In this example, the validate decorator function takes two parameters: target and key. The target parameter is a reference to the object being decorated (in this case, the Person class), and key is the name of the property being decorated (name).

The decorator function defines a getter and setter for the property that perform a validation check on the value. If the value is not a string, an error is thrown.

When we create a new instance of the Person class and try to set the name property to a non-string value, an error should be thrown:

Error: Property must be a string

This demonstrates that the decorator function successfully added a validation check to the name property.


Conclusion

Decorators are a powerful feature in TypeScript that allow you to add functionality to a class, method, or property. They can be used for a variety of purposes such as logging, validation, and more. In this tutorial, we explored decorators in TypeScript and saw how they can be used to enhance your code. With decorators, you can write more expressive, maintainable, and reusable code.