Decoradores en Typescript
Los decoradores en TypeScript son un tipo especial de funciones que permiten modificar el comportamiento de clases, métodos, propiedades y parámetros. Proveen una forma para agregar anotaciones al código y una sintaxis de meta-programación para declaraciones de clase y miembros.
Para agregar un decorador basta agregar el símbolo @
seguido del nombre del decorador justo antes de la clase o propiedad a la cual deseamos aplicarlo.
// función decoradora de clase
function classDecorator(target: any)
{
// function body
}
// función decoradora de propiedades
function propertyDecorator(target: any, propertyKey: string)
{
// function body
}
// función decoradora de métodos
function methodDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor)
{
// function body
}
@classDecorator
class MyClass {
@propertyDecorator
myProperty: string = '';
@methodDecorator
myMethod() {}
}
Cómo puedes observar en el ejemplo anterior la función decoradora tiene diferentes firmas dependiendo del contexto en el que se aplique. De manera general, podría tener hasta tres parámetros.
function decoratorFunction(target: any, propertyKey: string, descriptor: PropertyDescriptor)
{
// function body
}
En donde
- target Hace referencia la clase o propiedad a la cual se aplicará el decorador
- propertyKey Es el nombre de la propiedad en cuestión
- PropertyDescriptor Provee información adicional del descriptor
Ya veremos más adelante qué significa esto en cada contexto.
Los decoradores son una característica experimental de TypeScript, por lo cual debes activarla explícitamente con el flag experimentalDecorators
. También es importante mensionar que el "Property Descriptor" no estará disponible para targets por debado de ES5. Debido a esto, se recomienda combinar estas dos flags para tener un soporte completo de decoradores.
tsc --target ES5 --experimentalDecorators
Fábricas de Decoradores
Las fábricas de decoradores o decorator factories personalizan cómo un decorador es aplicado a una declaración. Para esto, es necesario envolver la función decoradora en otra función y retornarla. Si fuera un decorador de propiedad debería lucir de la siguiente forma.
function propertyDecorator()
{
// factory body
return function(target: any, propertyKey: string)
{
// decorator body
}
}
Una fábrica de decorador se utiliza casi de la misma manera que un decorador. Lo único que debes agregar son los paréntesis a la sentencia decoradora así.
@propertyDecorator()
myProperty: string = '';
Decoradores de propiedades
Para agregar un decorador de propiedad podemos agregar el símbolo @
seguido del nombre del decorador justo antes de la definición de la propiedad. Veamos el siguiente ejemplo.
function currencyDecorator(target: any, propertyKey: string)
{
console.log('decorator called!');
}
class BankAccount {
@currencyDecorator
public amount: string;
constructor(amount: string) {
this.amount = amount;
}
}
Al observar la clase vemos que la propiedad amount
está precedida por @currencyDecorator
el cual es el decorador. La función decoradora por convención, siempre debe llevar dos parámetros.
function currencyDecorator(target: any, propertyKey: string)
En donde
- target Hace referencia al prototipo de la clase a la cual pertenece el método
- propertyKey Es el nombre del método
Modifiquemos el ejemplo anterior para agregar el código de la moneda en USD. Observa cómo se usa Object.defineProperty
para modificar el comportamiento del getter y el setter.
function currencyDecorator(target: any, propertyKey: string)
{
// target en este contexto es el objeto BankAccount
// así que estamos accediendo a BankAccount.amount
let value = target[propertyKey];
const getter = () => {
return `${value} USD`;
};
const setter = (newValue: string) => {
value = newValue;
};
Object.defineProperty(target, propertyKey, {
get: getter, // aquí definimos el getter
set: setter, // aquí definimos el setter
enumerable: true, // la propiedad se puede iterar
configurable: true, // la propiedad no se puede eliminar
});
}
class BankAccount {
@currencyDecorator
public amount: string;
constructor(amount: string) {
this.amount = amount;
}
}
const account = new BankAccount('100');
console.log('The amount is: ' + account.amount);
Observa cómo la lógica para agregar la moneda está en la función decoradora. La salida del anterior programa es la siguiente:
The amount is: 100 USD
Todavía podemos ir un poco más allá y utilizar una fábrica de decorador para agregar el parámetro de la moneda y mostrarla según sea el caso.
function currencyDecorator(currencyCode: string) {
return function (target: any, propertyKey: string)
{
let value = target[propertyKey];
const getter = () => {
return `${value} ${currencyCode}`;
};
// el mismo resto del código aquí ....
}
}
class BankAccount {
@currencyDecorator('CAD')
public amount: string;
constructor(amount: string) {
this.amount = amount;
}
}
const account = new BankAccount('100');
console.log('The amount is: ' + account.amount);
Hemos simplificado un poco el código ya que es casí exactamente el mismo. Observa como en el ejemplo anterior se pasa el parámetro CAD al decorador para ser retornado junto con el valor de la currency. La salida en este caso es la siguiente:
The amount is: 100 CAD
Decoradores de métodos
Para agregar un decorador de método podemos agregar el símbolo @
seguido del nombre del decorador justo antes de la firma del método. Veamos el siguiente ejemplo.
function timed(target: any, propertyKey: string, descriptor: PropertyDescriptor)
{
console.log('decorator called!')
}
class Example {
@timed
hello(s: string) { }
}
Antes que nada, al observar la clase veamos que el método hello
está precedido en la anterior línea por @first
el cual es el decorador. La función decoradora por convención, siempre debe llevar tres parámetros.
target: any, propertyKey: string, descriptor: PropertyDescriptor
En donde
- target Hace referencia al prototipo de la clase a la cual pertenece el método
- propertyKey Es el nombre del método
- PropertyDescriptor Provee información adicional del descriptor
Modifiquemos el ejemplo anterior para concatenar el tiempo en el cual se realizó el saludo. Observa cómo modificar descriptor.value
es modificar la función original para envolverla en una lógica adicional. La función original es ejecutada en func.apply(this, arguments)
.
function timed(target: any, propertyKey: string, descriptor: PropertyDescriptor)
{
// guardamos la definición de función original
const func = descriptor.value;
// definición de la nueva función
descriptor.value = function()
{
const time: Date = new Date();
const timeString: string = time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
// retornamos el time más el resultado original de la función
return timeString + ' ' + func.apply(this, arguments);
};
}
class Example {
@timed
hello(name: string): string {
return 'Hi, ' + name
}
}
const salutation = (new Example()).hello('Steve');
console.log(salutation);
Observa como la función hello
no pierde su esencia y su único objetivo es devolver un saludo al nombre de la persona pasado como parámetro. El decorador @timed
agrega al inicio del saludo la hora y minuto en el cual se "envió" sin agregar código o lógica demás al método hello
. La salida del anterior programa es similar a la siguiente:
18:46 PM Hi, Steve
Todavía podemos ir un poco más allá y utilizar una fábrica de decorador para agregar el parámetro del formato de la fecha según sea el caso.
type TwoDigit = "2-digit" | "numeric" | undefined;
function timed(format: { hour: TwoDigit, minute: TwoDigit }) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor)
{
const func = descriptor.value;
descriptor.value = function()
{
const time: Date = new Date();
const timeString: string = time.toLocaleTimeString([], format);
return timeString + ' ' + func.apply(this, arguments);
};
}
}
class Example {
@timed({ hour: '2-digit', minute: undefined })
hello(name: string): string {
return 'Hi, ' + name
}
}
const salutation = (new Example()).hello('Steve');
console.log(salutation);
En este caso, el parámetro del decorador es un objeto que cumple con la definición de formato de fecha. También se ha creado un tipo adicional para guardar los valores posibles para hora y fecha. La salida del programa en este caso es la siguiente:
10 PM Hi, Steve