7 min to read
Typescript decorators!
 
                
                
                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?
Funciones.
Nada más ni nada menos que funciones que podemos aplicar en nuestras clases:
- Class
- Class Property
- Class Method
- Class Accessor
- 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:
- Class Decorators
- Property Decorators
- Method Decorators
- Accessor Decorators
- 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:
- target: función constructora de la clase o instancia
- propertyKey: el nombre de la property
- 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.

Referencias
Post e info complementaria con más ejemplos acá
Happy coding!


 
            
        