Decoradores en Typescript

Author
Por Darío Rivera
Publicado el 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

Acerca de Darío Rivera

Author

Application Architect at Elentra Corp . Quality developer and passionate learner with 10+ years of experience in web technologies. Creator of EasyHttp , an standard way to consume HTTP Clients.

LinkedIn Twitter Instagram

Sólo aquellos que han alcanzado el éxito saben que siempre estuvo a un paso del momento en que pensaron renunciar.