Typescript decorators!

Featured image

Typescript Decorators

How are you doing? First post January 2023! Today’s post is dedicated to the TS decorators. As always I’m sharing the official documentation here

What are they?

enter image description here Functions. Nothing more and nothing less than functions that we can apply in our classes:

  1. Class
  2. Class Property
  3. Class Method
  4. Class Accessor
  5. Class Method Parameter

Where did they come from? Well, thanks to the class management in TS and ES6, we can now talk about decorators. They are an experimental feature in TS, and in ES they are still in stage 2 here

Syntax

Let’s look at the most basic example of a decorator:

function decorator() {
  console.log("I am a decorator!");
}

@Decorator
class A {}

Decorator types

We can implement 5 types of decorators:

  1. Class Decorators
  2. Property Decorators
  3. Method Decorators
  4. Accessor Decorators
  5. Parameter Decorators

Let’s see an example where these 5 decorators are reflected:

@classDecorator
class User {
  @propertyDecorator
  name: string;

  @methodDecorator
  walk(
    @parameterDecorator
    speed: number
  ) {}

  @accessorDecorator
  get age() {}
}

Decorators Composition

We can apply multiple decorators to the same target. In that case, we will have to take into account the order of evaluation of these compound decorators, similar to the composition of functions in mathematics, to see the order of evaluation here We can decorate on one or multiple lines:

    @f @g x

    @f
    @g
    x

Method decorator

Let’s take a look at its structure:

type MethodDecorator = <T>(
  target: Object,
  propertyKey: string | symbol,
  descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;

The params it receives:

  1. target: the constructor function of the class or instance.
  2. propertyKey: the name of the property.
  3. descriptor: the property descriptor, the config of the object.

So, we can say that what differentiates the methods decorators from, for example, the property decortators, in principle, is this param descriptor. This descriptor is what will allow us to overwrite the original implementation in order to inject our logic.

Sample 1:

//Decorator
function logger(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const original = descriptor.value;

  descriptor.value = function (...args) {
    console.log("params: ", ...args);
    const result = original.call(this, ...args);
    console.log("result: ", result);
    return result;
  };
}

// method decorator
class C {
  @logger
  add(x: number, y: number) {
    return x + y;
  }
}

const c = new C();
c.add(1, 2);
// -> params: 1, 2
// -> result: 3

In this example we see that by applying the @logger decorator to the add() method of the C class, every time we invoke that method, the params and the result of the operation will be logged.

Sample 2:

Decorator:

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

The only thing we do is to modify the enumerable prop of the property descriptor. We apply the decorator to the method of our class:

class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  @enumerable(false)
  greet() {
    return "Hello, " + this.greeting;
  }
}

Sample 3:

Another case that I personally found very interesting to apply, is for example, to add a cache for some method requests. In this case, we not only apply the structure and logic that corresponds to the method decorator, but we are also going to combine everything with some rxjs magic to transmit values and store them for a certain amount of time. I share with you a post with the guide to review it and/or implement it here

Composition: Parameter + Method decorators

In the example we are going to apply 2 decorators to mark params as required (@required) and to validate the args before invoking the original method (@validate) .

Example of both decorators:

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

function validate(
  target: any,
  propertyName: string,
  descriptor: TypedPropertyDescriptor<Function>
) {
  let method = descriptor.value!;
  descriptor.value = function () {
    let requiredParameters: number[] = Reflect.getOwnMetadata(
      requiredMetadataKey,
      target,
      propertyName
    );
    if (requiredParameters) {
      for (let parameterIndex of requiredParameters) {
        if (
          parameterIndex >= arguments.length ||
          arguments[parameterIndex] === undefined
        ) {
          throw new Error("Missing required argument.");
        }
      }
    }
    return method.apply(this, arguments);
  };
}

Implementation:

class BugReport {
  type = "report";
  title: string;
  constructor(t: string) {
    this.title = t;
  }

  @validate // method decorator
  print(@required verbose: boolean) {
    // param decorator
    if (verbose) {
      return `type: ${this.type}\ntitle: ${this.title}`;
    } else {
      return this.title;
    }
  }
}

Advantages

Decorators help us to improve our code with a declarative, easy to read syntax. With a single line we can apply logic and functions without the need to write additional code.

It is interesting to analyze which cases and scenarios of our projects, may be candidates to implement these decorators, especially when we notice some logic that we can abstract and reuse.

enter image description here

References

Post and complementary info with more examples here Happy coding!

enter image description here