DEV Community

Cover image for Decoradores en typescript (parte 2)
leobar37
leobar37

Posted on

Decoradores en typescript (parte 2)

Decoradores en typescript

Hola, en este post veremos un poco sobre decoradores, específicamente en TypeScript, en el post anterior vimos sobre el patrón en decorador en sí y cuál es su beneficio.

En Frameworks como Angular o (Nestjs)[https://nestjs.com/] se ve a cada momento los decoradores, para definir componentes, servicios, controladores etc. y todo esto hace que sea como mágico, pero la gran verdad es que detrás de una simple anotación puede estar escondida mucha lógica de por medio. Es un poco curioso porque esto es una característica experimental en TypeScript y esta como propuesta en javascript.

En TypeScript un decorador es un tipo especial de declaración que puede ser aplicado a clases, métodos, propiedades y parámetros y es llamada en tiempo de ejecución con información acerca de la declaración decorada.

En este post se va hablar de 3 tipos de decorador:

  • Class
  • Property
  • Method

Decorador de clase

Con este decorador es posible interceptar la clase y hacer algo con ella.

Por ejemplo; podemos agregar propiedades y metodos a la clase.

function print(target: any) {
  return class extends target {
    printVersion = 1;

    print() {
      const toString = Object.entries(this).reduce(
        (prev, curr) => prev + `${curr[0]} : ${curr[1]}\n`,
        ''
      );
      console.log(toString);
    }
  } as any;
}
Enter fullscreen mode Exit fullscreen mode

La función print es un decorador el cual debe ser llamando con la siguiente anotación @decorator.

Todos los decoradores de clase van a recibir como parámetro la clase sobre el cual se está aplicando el decorador para poder hacer algo con ella en este caso, estamos extendiendo esta clase, agregando el método print.

interface Person {
  print(): void;
}

@print
class Person {
  name = 'leo';
  age = 10;
  constructor() {}
}

const person = new Person();

person.print();
/**
name : leo
age : 10
printVersion : 1
 */

Enter fullscreen mode Exit fullscreen mode

La razón por la cual hay existe la interfaz Person es porque los decoradores pueden agregar funcionalidades, pero no pueden comunicar el tipado de estas funcionalidades. Pero en TypeScript existe combinación de declaraciones, lo cual permite combinar dos o más declaraciones con el mismo nombre.

Para entenderlo un poco mejor vamos a alterar el constructor de la clase a través de un decorador.

function noise(target: any) {
  return class extends target {
    constructor(...args: any[]) {
      console.log('Hello friend I am instantiating');
      super(args);
      console.log('i am already instantiated');
    }
  } as any;
}
Enter fullscreen mode Exit fullscreen mode

El decorador noise modifica el constructor de la clase objetivo agregando un mensaje antes y después.

 

@noise
@print
class Person {
  name = 'leo';
  age = 10;
  constructor() {
    console.log(':o');
  }
}

const person = new Person();

person.print();

/*
Hello friend I am instantiating
:o
i am already instantiated
name : leo
age : 10
printVersion : 1
*/
Enter fullscreen mode Exit fullscreen mode

Decorador de propiedades

Así como podemos interceptar las clases, también podemos interceptar las propiedades, para que una función pueda ser utilizado como decorador de propiedad, tiene que recibir dos argumentos la clase y el nombre de la propiedad.

function propertyDecorator(target: any, key: string | Symbol) {

}

Enter fullscreen mode Exit fullscreen mode

Veamos un ejemplo sencillo:

function uppercase(target: Object, key: string | symbol) {
  let value: string | null = (target as any)[key];

  const setter = (val: string) => {
    value = val.toLocaleUpperCase();
  };
  const getter = () => {
    return value;
  };
  Object.defineProperty(target, key, {
    enumerable: true,
    configurable: true,
    set: setter,
    get: getter
  });
}

Enter fullscreen mode Exit fullscreen mode

Este decorador tiene como función , convertir en mayúscula el valor de la propiedad a la cual es aplicado, pero se puede notar algo nuevo defineProperty, el cual es una función que nos permite definir la descripción de una propiedad, puedes leer más de esto aquí.

Veamos una pequeña explicación de las propiedades que utilice.

ennumerable: Permite que esta propiedad sea visible en la enumeración de las propiedades, Es decir si es false no aparecerá cuando se utilice con Object.keys o For .. in por ejemplo.

configurable:
Si es true , el descriptor de esta propiedad puede ser modificado.

get:
Esta función es llamada cuando se pida la propiedad.

set:

Es invocado al agregar el valor a la propiedad.

Ahora ya podemos utilizar el decorador.

@noise
@print
class Person {
  @uppercase
  name = 'leo';

  age = 10;
  constructor() {
    console.log(':o');
  }
}

const person = new Person();

// person.print();

console.log(person.name);

/*
output:
Hello friend I am instantiating
:o
i am already instantiated
LEO
*/

Enter fullscreen mode Exit fullscreen mode

Ahora, ¿qué pasaría si quiero utilizar ese mismo decorador para poner en mayúscula solo la primera letra?, tendría que pasarle una opción que indique que quiero todo en mayúscula o solo la primera letra, para eso vamos a crear una función que nos retorne el decorador lo que sería un Decorator factory.

function uppercase({ first }: UppercaseOptions) : PropertyDecorator {
  return function (target: Object, key: string | symbol) {
    let value: string | null = (target as any)[key];

    const setter = (val: string) => {
      if (!!first) {
        const firstLetter = val[0].toLocaleUpperCase();
        value = firstLetter + val.substr(1, val.length);
      } else {
        value = val.toLocaleUpperCase();
      }
    };
    const getter = () => {
      return value;
    };
    Object.defineProperty(target, key, {
      enumerable: true,
      configurable: false,
      set: setter,
      get: getter
    });
  };
}
Enter fullscreen mode Exit fullscreen mode

Ahora tenemos una función que retorna un decorador, incluso se ha tipado el retorno con PropertyDecorator, esto no es un tipo creado por mí, existe el tipado para cada unos de los decoradores dentro de TypeScript.

declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;
Enter fullscreen mode Exit fullscreen mode

Decorador de métodos

Del mismo manera que podemos interceptar clases y propiedades, también se puede interceptar los métodos, para que una función se convierta en un decorador de métodos, es casi lo mismo que el decorador de propiedades a diferencia que este recibe un parámetro más, descriptor, del cual ya sé menciono previamente.

Para ver un ejemplo vamos a agregar un método llamado food.

@noise
@print
class Person {
  @uppercase({ first: true })
  name = 'leo';

  age = 10;
  constructor() {
    console.log(':o');
  }

  eat(food: string) {
    console.log('I am eating ', food);
  }
}
Enter fullscreen mode Exit fullscreen mode

Este recibe un parámetro el cual es el alimento, pero queremos validar que el usuario no pase como parámetro una bebida, vamos a crear un decorador para validar eso.

function foodGuard(): MethodDecorator {
  return function (target, key, descriptor) {
    const originalFn = descriptor.value as any;
    (descriptor.value as any) = function (this: any, ...args: any[]) {
      const value = args[0];
      if (value == 'water') {
        throw new Error('Water is a drink');
      }
      originalFn.apply(this, args);
    };
  };
}
Enter fullscreen mode Exit fullscreen mode

El decorador foodGuard tiene como funcionalidad verificar que el parámetro no sea agua, si esto sucede arroja un error. En este caso no se ha utilizado get y set se ha establecido la propiedad value el cual es el valor de la propiedad en este caso, una función.

@noise
@print
class Person {
  @uppercase({ first: true })
  name = 'leo';

  age = 10;
  constructor() {
    console.log(':o');
  }

  @foodGuard()
  eat(food: string) {
    console.log('I am eating ', food);
  }
}

const person = new Person();


person.eat('water'); // lauch a error

console.log(person.name);

Enter fullscreen mode Exit fullscreen mode

Hasta aquí una pequeña introducción a decoradores, para poder disfrutar más de ellos te invito a leer los siguientes artículos.

Top comments (0)