TypeScript - Decorators

Hello, aspiring programmers! Today, we're going to embark on an exciting journey into the world of TypeScript Decorators. Don't worry if you're new to programming – I'll guide you through this concept step by step, just like I've done for countless students over my years of teaching. So, let's dive in!

TypeScript - Decorators

What are Decorators?

Before we jump into the nitty-gritty, let's understand what decorators are. Imagine you have a plain cupcake. Decorators are like the sprinkles, frosting, and cherry on top that make your cupcake extra special. In TypeScript, decorators add extra functionality to your classes, methods, properties, and parameters.

Using Decorators in TypeScript

To start using decorators in TypeScript, you need to enable them in your tsconfig.json file. It's like turning on the oven before baking your cupcakes!

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

Decorator Syntax

Now, let's look at how we write decorators. It's simpler than you might think!

function simpleDecorator(target: any) {
  console.log("I'm a simple decorator!");
}

@simpleDecorator
class MyClass {
  // Class implementation
}

In this example, simpleDecorator is like a sticker we're putting on our MyClass. Every time we use MyClass, it will log "I'm a simple decorator!" to the console.

Decorator Factories

Sometimes, we want our decorators to be customizable. That's where decorator factories come in. They're like a machine that produces decorators based on our specifications.

function decoratorFactory(message: string) {
  return function (target: any) {
    console.log(message);
  }
}

@decoratorFactory("Hello, I'm a custom decorator!")
class MyClass {
  // Class implementation
}

Here, decoratorFactory is creating a decorator that logs our custom message.

Decorator Composition

We can use multiple decorators on a single target. It's like adding multiple toppings to our cupcake!

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

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

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

In this case, the decorators are applied from bottom to top: second() then first().

Why Use Decorators?

Decorators are incredibly useful for:

  1. Adding metadata to your code
  2. Modifying the behavior of classes and methods
  3. Implementing aspects of Aspect-Oriented Programming
  4. Creating reusable code that can be easily applied to multiple classes

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 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 prevents the Greeter class from being modified after its definition.

Method Decorators

Method decorators can be used to modify, observe, or replace a method definition.

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(false) decorator makes the greet method non-enumerable.

Accessor Decorators

Accessor decorators are applied to the property descriptor for the accessor and can be used to observe, modify, or replace an accessor's definitions.

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(false) decorator makes the x and y accessors non-configurable.

Property Decorators

Property decorators are used to observe, modify, or replace a property definition.

function format(formatString: string) {
  return function (target: any, propertyKey: string): any {
    let value = target[propertyKey];

    const getter = function () {
      return `${formatString} ${value}`;
    };

    const setter = function (newVal: string) {
      value = newVal;
    };

    return {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true
    };
  };
}

class Greeter {
  @format("Hello,")
  greeting: string;
}

const greeter = new Greeter();
greeter.greeting = "World";
console.log(greeter.greeting); // Outputs: "Hello, World"

Here, the @format decorator adds a prefix to the greeting property.

Parameter Decorators

Parameter decorators are used to observe, modify, or replace a parameter definition.

function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
  let existingRequiredParameters: number[] = Reflect.getOwnMetadata("required", target, propertyKey) || [];
  existingRequiredParameters.push(parameterIndex);
  Reflect.defineMetadata("required", existingRequiredParameters, target, propertyKey);
}

class Greeter {
  greet(@required name: string) {
    return "Hello " + name;
  }
}

In this example, the @required decorator marks the name parameter as required.

Decorator Methods

Here's a table summarizing the different types of decorators and their methods:

Decorator Type Method
Class Decorator declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
Method Decorator declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
Accessor Decorator declare type AccessorDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
Property Decorator declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
Parameter Decorator declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;

And there you have it! We've covered the basics of TypeScript decorators. Remember, like learning to bake the perfect cupcake, mastering decorators takes practice. Don't be afraid to experiment and make mistakes – that's how we learn and grow as programmers. Happy coding!

Credits: Image by storyset