Decorators in TypeScript: A Comprehensive Guide

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

Introduction

TypeScript decorators provide a powerful and expressive way to add annotations and a meta-programming syntax for class declarations and members. This guide will explore decorators in-depth, helping you to understand and leverage them in your TypeScript projects.

What are Decorators?

Decorators are a stage 2 proposal for JavaScript and they are available as an experimental feature of TypeScript. They are special kinds of declarations 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.

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

Enabling Decorators in TypeScript

To start using decorators in TypeScript, you need to enable them in your tsconfig.json by setting the experimentalDecorators option to true.

{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true
  }
}

Class Decorators

Class decorators are applied to the constructor of the class and can be used to observe, modify, or replace a class definition.

function logged(constructorFn: Function) {
  console.log(constructorFn);
}

@logged
class Person {
  constructor() {
    console.log('Hi!');
  }
}

Method Decorators

Method decorators are applied to the property descriptor of the method, and can be used to change its behavior or replace it altogether.

function editable(value: boolean) {
  return function (target: any, propName: string, descriptor: PropertyDescriptor) {
    descriptor.writable = value;
  };
}

class Project {
  projectName: string;

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

  @editable(false)
  calcBudget() {
    console.log('Calculating budget...');
  }
}

Property Decorators

Property decorators are applied to properties of classes and can be used to modify their behavior or inject additional metadata.

function format(formatString: string) {
  return function (target: any, propertyName: string): void {
    let _val = target[propertyName];
    
    const getter = function () {
      return _val;
    };

    const setter = function (newVal: string) {
      _val = formatString.replace('%s', newVal);
    };

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

class User {
  @format('Hello, %s')
  name: string;

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

Parameter Decorators

Parameter decorators are applied to the function for a class constructor or method declaration and can be used to modify the behavior of the function or inject parameters.

function printInfo(target: any, methodName: string, paramIndex: number) {
  console.log('target', target);
  console.log('methodName', methodName);
  console.log('paramIndex', paramIndex);
}

class Course {
  name: string;

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

  printStudentNumbers(mode: string, @printInfo printAll: boolean) {
    if (printAll) {
      console.log(10000);
    } else {
      console.log(2000);
    }
  }
}

Decorator Factories

Decorator factories provide a way to customize how a decorator is applied to a declaration. They are simply functions that return the expression that will be called by the decorator at runtime.

function logging(value: boolean) {
  return value ? logged : null;
}

@logging(true)
class Car {
  //...
}

Decorators Composition

Multiple decorators can be applied to a declaration, as in JavaScript, and they are evaluated in the reverse order to which they are declared.

@logged
@sealed
class Boat {
  //...
}

Accessor Decorators

Accessor decorators are applied to the property descriptor for an accessor and can be used to change the attribute of the accessor, such as whether it’s enumerable or configurable.

class Job {
  private _title: string;

  constructor(title: string) {
    this._title = title;
  }

  @editable(true)
  get title() {
    return this._title;
  }

  @editable(false)
  set title(value: string) {
    this._title = value;
  }
}

Reflection

Reflection is a key concept that works hand-in-hand with decorators. TypeScript includes metadata support which allows us to add annotations and perform introspection on our declarations. You enable this by setting the emitDecoratorMetadata compiler option to true.

@printMetadata
class Plane {
  color: string = 'red';

  constructor() {
    //...
  }

  fly(): void {
    console.log('Whirrr!');
  }
}

Decorators and Inheritance

It’s important to understand how decorators affect the inheritance of classes. If you have decorators with side effects for properties and methods, these may or may not apply to derived classes depending on how you implement them.

class Biplane extends Plane {
  fly(): void {
    console.log('Zooooom!');
  }
}

Advanced Decorator Usage

Advanced decorator scenarios can involve combining decorators with other TypeScript features like mixins or using them in abstract classes and interfaces.

// A decorator function that takes a constructor and returns a new constructor
function Mixin<T extends Constructor>(base: T) {
  // The new constructor class
  return class extends base {
    // Add some new properties and methods
    public mixinProperty = "Hello";
    public mixinMethod() {
      console.log("Mixin method");
    }
  };
}

// A dummy constructor type
type Constructor = new (...args: any[]) => {};

// A base class to apply the mixin
class Foo {
  public foo() {
    console.log("Foo method");
  }
}

// Apply the mixin decorator to the base class
@Mixin
class Bar extends Foo {
  public bar() {
    console.log("Bar method");
  }
}

// Create an instance of the decorated class
const bar = new Bar();

// Use the inherited methods from Foo
bar.foo(); // Foo method

// Use the mixed in properties and methods
console.log(bar.mixinProperty); // Hello
bar.mixinMethod(); // Mixin method

// Use the own methods from Bar
bar.bar(); // Bar method

Conclusion

Decorators in TypeScript offer a powerful set of tools for writing concise and descriptive code. They enable annotation and modification of classes and properties at design time which provides great flexibility in how our code is executed at runtime.