Decoradores en typescript (parte 2)
Published: 2021-10-30 04:10:51

# 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](https://angular.io/) 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](https://github.com/tc39/proposal-decorators) 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.



```ts
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;
}
```
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`.

```ts
interface Person {
  print(): void;
}

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

const person = new Person();

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

```

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.

```ts
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;
}
```

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


```ts
@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
*/
```

## 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. 

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

```
Veamos un ejemplo sencillo:

```ts
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
  });
}

```

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í](https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor).


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.

```ts
@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
*/

```

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**.

```ts
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
    });
  };
}
```


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.

```ts
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;
```

## 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`.

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

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

  eat(food: string) {
    console.log('I am eating ', food);
  }
}
```
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.

```ts
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);
    };
  };
}
```

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.

```ts
@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);

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

- [Reflect-metadata](https://ichi.pro/es/introduccion-al-paquete-reflect-metadata-y-su-propuesta-ecmascript-149087984647560)
- [Custom decorators in angular](https://netbasal.com/inspiration-for-custom-decorators-in-angular-95aeb87f072c)
- [Documentación](https://www.typescriptlang.org/docs/handbook/decorators.html)