Typescript decorators!

Featured image

Typescript Decorators

Buenass! como andan? Primer post enero 2023! El post de hoy va dedicado a los TS decorators. Como siempre comparto la docu oficial por acá

Qué son?

enter image description here Funciones. Nada más ni nada menos que funciones que podemos aplicar en nuestras clases:

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

De donde salieron? Bueno, gracias al manejo de clases en TS y ES6, podemos ahora hablar de decorators. Son una feature experimental en TS, y en ES aún están en stage 2 aquí

Syntax

Veamos el ejemplo más básico de un decorator:

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

@Decorator
class A {}

Tipos de decoradores

Podemos implementar 5 tipos de decoradores:

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

Veamos un ejemplo donde se reflejan estos 5 decoradores:

@classDecorator
class User {
  @propertyDecorator
  name: string;

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

  @accessorDecorator
  get age() {}
}

Composición de decoradores

Podemos aplicar múltiples decoradores a un mismo target. En ese caso, tendremos que tener en cuenta el orden de evaluación de estos decoradores compuestos, similar a la composición de funciones en matemática, para ver el orden de evaluación acá Podemos decorar en una o en múltiples líneas:

    @f @g x

    @f
    @g
    x

Method decorator

Veamos la estructura que tiene:

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

Los params que recibe:

  1. target: función constructora de la clase o instancia
  2. propertyKey: el nombre de la property
  3. descriptor: es la property descriptor, la config del objeto.

Entonces, podemos decir que lo que diferencia a los methods decorators de, por ejemplo, los property decortators, en principio, es este param descriptor. Este descriptor, es quien nos va a permitir sobreescribir la implementación original para poder inyectar nuestra lógica.

Ejemplo 1:

Veamos un ejemplo de código:

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

// Aplicamos decorador al método add
class C {
  @logger
  add(x: number, y: number) {
    return x + y;
  }
}

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

En este ejemplo vemos que aplicando el decorador @logger al método add() de la clase C, cada vez que invoquemos ese método, se loguearán los params y el resultado de la operación.

Ejemplo 2:

Decorador:

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

Lo único que hacemos es modificar la prop enumerable de la property descriptor. Aplicamos el decorador al método de nuestra clase:

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

Ejemplo 3:

Otro caso que personalmente me pareció muy interesante de aplicar, es por ejemplo, agregar una cache para algunos métodos requests. En este caso, no sólo aplicamos la estructura y lógica que corresponde al method decorator, sino que también vamos a combinar todo con un poco de magia rxjs para transmitir valores y almacenarlos por una cantidad de tiempo. Les comparto un post con la guía para repasarlo y/o implementarlo acá

Composición: Parameter + Method decorators

En el ejemplo vamos a aplicar 2 decoradores para marcar params como requeridos (@required) y para validar los args antes de invocar al método original (@validate) .

Ejemplo de ambos decoradores:

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

Ejemplo de implementación:

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

Ventajas

Los decoradores nos ayudan a mejorar nuestro código con una sintaxis declarativa, fácil de leer. Con una sóla línea podemos aplicar lógica y funciones sin la necesidad de escribir código adicional.

Es interesante analizar qué casos y escenarios de nuestros proyectos, pueden ser candidatos para implementar estos decoradores, sobre todo cuando notamos cierta lógica que podemos abstraer y reutilizar.

enter image description here

Referencias

Post e info complementaria con más ejemplos acá

Happy coding!

enter image description here