Decorators in TypeScript

Guide to Using Decorators in TypeScript

Decorators in TypeScript

Welcome back to our TypeScript series!

Today, we’ll explore decorators, a powerful feature that allows you to modify classes, methods, properties, and parameters. Decorators provide a way to add annotations and a meta-programming syntax for class declarations and members. They are widely used in frameworks like Angular for dependency injection, routing, and more.

What are Decorators?

Decorators are special functions that can be attached to classes, methods, accessors, properties, or parameters. They provide a way to add behavior to these elements in a declarative way. In TypeScript, decorators are an experimental feature and require the experimentalDecorators compiler option to be enabled in tsconfig.json.

Enabling Decorators

To use decorators, you need to enable them in your tsconfig.json file:

{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}

Types of Decorators

There are several types of decorators in TypeScript:

  1. Class Decorators

  2. Method Decorators

  3. Accessor Decorators

  4. Property Decorators

  5. Parameter Decorators

1. Class Decorators

A class decorator is a function that takes a class constructor as an argument and can modify or replace the class.

Example: Class Decorator

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

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

  greet() {
    return `Hello, ${this.greeting}`;
  }
}

In this example, the sealed decorator seals the Greeter class, preventing new properties from being added to it.

2. Method Decorators

A method decorator is a function that takes three arguments: the target (class prototype), the property key, and the property descriptor.

It can modify the method or its descriptor.

Example: Method Decorator

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}`;
  }
}

Here, the enumerable decorator sets the enumerable property of the greet method descriptor to false.

3. Accessor Decorators

An accessor decorator is similar to a method decorator but is applied to getters and setters. It takes the same arguments as a method decorator.

Example: Accessor Decorator

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

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

  constructor(x: number, y: number) {
    this._x = x;
    this._y = y;
  }

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

  @configurable(false)
  get y() {
    return this._y;
  }
}

In this example, the configurable decorator sets the configurable property of the accessor descriptors to false.

4. Property Decorators

A property decorator is a function that takes two arguments: the target (class prototype) and the property key. It can modify the property but does not have access to the property descriptor.

Example: Property Decorator

function logProperty(target: any, key: string) {
  let _val = target[key];

  const getter = () => {
    console.log(`Get: ${key} => ${_val}`);
    return _val;
  };

  const setter = (newVal) => {
    console.log(`Set: ${key} => ${newVal}`);
    _val = newVal;
  };

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

class Person {
  @logProperty
  name: string;

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

let p = new Person("Alice");
p.name = "Bob"; // Output: Set: name => Bob
console.log(p.name); // Output: Get: name => Bob

Here, the logProperty decorator logs get and set operations on the name property.

5. Parameter Decorators

A parameter decorator is a function that takes three arguments: the target (class prototype), the property key, and the parameter index. It is used to add metadata about the parameters.

Example: Parameter Decorator

function logParameter(target: any, propertyKey: string, parameterIndex: number) {
  const metadataKey = `log_${propertyKey}_parameters`;

  if (Array.isArray(target[metadataKey])) {
    target[metadataKey].push(parameterIndex);
  } else {
    target[metadataKey] = [parameterIndex];
  }
}

class Greeter {
  greeting: string;

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

  greet(@logParameter name: string) {
    return `Hello, ${name}! ${this.greeting}`;
  }
}

In this example, the logParameter decorator logs the parameter index of the greet method.

Combining Decorators

You can combine multiple decorators by stacking them, applying each decorator from top to bottom.

Example: Combining Decorators

function first() {
  console.log('first(): factory evaluated');
  return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log('first(): called');
  };
}

function second() {
  console.log('second(): factory evaluated');
  return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log('second(): called');
  };
}

class ExampleClass {
  @first()
  @second()
  method() {}
}

The output will be:

first(): factory evaluated
second(): factory evaluated
second(): called
first(): called

Practical Use Cases

Decorators are especially useful in frameworks and libraries for:

  • Metadata: Adding metadata to classes and members.

  • Dependency Injection: Annotating services and dependencies.

  • Validation: Adding validation rules to properties and methods.

  • Logging: Logging method calls and property access.

Summary

Today, we explored decorators in TypeScript, covering class decorators, method decorators, accessor decorators, property decorators, and parameter decorators. Decorators provide a powerful way to add behavior to your code in a declarative manner. Understanding and using decorators effectively can help you write more maintainable and expressive code.

Next time, we’ll dive into the TypeScript Compiler API. Stay tuned!