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!