Decorador de Clase en TypeScript

De ChuWiki


¿Qué son los decoradores de clase?[editar]

Los decoradores de clase en TypeScript son funciones que se aplican a la declaración de una clase usando el símbolo @. Estas funciones pueden modificar el comportamiento de la clase o registrar información adicional sobre ella.

Ejemplo básico[editar]

Aquí tienes un ejemplo básico de cómo se utilizan

// Definición de un decorador de clase
function SaludarClase(constructor: Function) {
    console.log("Hola desde el constructor");
}

// Aplicando el decorador a una clase
@SaludarClase
class Ejemplo {
    constructor() {
        console.log("Hola desde el constructor de Ejemplo");
    }
}

Para que funcione esta sintaxis de decorador, debes tener habilitadas las opciones experimentalDecorators de TypeScript. Para ello, ejecuta con la opción --experimentalDecorators o bien, en el fichero tsconfig.json de tu proyecto, habilita la opción "experimentalDecorators": true, en el apartado de "compilerOptions": { ... }

Hemos creado la función SaludarClase que hará de decorador. Recibe como parámetro la clase que más adelante será decorada. En esta función sólo sacamos un log por console. Más abajo creamos una clase Ejemplo y la decoramos con @SaludarClase. En el constructor de esta clase ponemos un log.

Si ejecutamos el código y sin necesidad de hacer ningún new de la clase, vemos que sale el log de la función decorador "Hola desde el constructor". El motivo es que se llama a esta función cuando se hace la declaración de la clase con su decorador. Y sólo se llama una vez.

Si necesitamos hacer algo cada vez que se llama al constructor, debemos hacer algo distinto.

Observar, Modificar o Reemplazar la clase decorada[editar]

Para poder hacer algo distinto cada vez que se llama al constructor de la clase, el método decorador cambia un poco la sintaxis. Sería algo como esto

// Definición de un decorador de clase
function SaludarClase<T extends { new (...args: any[]): {} }>(originalClass: T) {
    console.log("Hola desde el constructor");
}

// Aplicando el decorador a una clase
@SaludarClase
class Ejemplo {
    constructor() {
        console.log("Hola desde el constructor de Ejemplo");
    }
}

Ahora la función SaludarClase es un genérico. Básicamente ponemos entre <> el tipo que esperamos como parámetro, T extends { new (...args: any[]): {}. Este tipo T será cualquier cosa que herede de algo que se pueda instanciar con new ( { new , pasándole un número indefinido de parámetros de cualquier tipo (...args: any[]) y que devuelva un objeto : {}. Esta es justo la definición de una clase, que se puede instanciar con new y nos devuelve la instancia de la clase.

El parámetro de la función decoradora será por tanto, originalClass: T que corresponde a una clase. Y ahí nos llamarán una sola vez, con la clase que ha sido decorada.

Y para que el decorador haga algo cada vez que se instancie a la clase, debemos devolver en la función decoradora una clase hija de la que nos han pasado que tenga el comportamiento esperado. Por ejemplo, si queremos que saque un log cada vez que se haga un new de la clase, podemos hacer esto

// Definición de un decorador de clase
function SaludarClase<T extends { new (...args: any[]): {} }>(originalClass: T) {
    console.log("Hola desde el constructor");
    return class extends originalClass {
        constructor(...args: any[]){
            super(args);
            console.log(originalClass); // Saca por pantalla [class NombreClaseQueSeInstancia]
        }
    }
}

Hemos mantenido el log que ya teníamos. Hacemos un return de una nueva clase que hereda de originalClass. Le añadimos un constructor con cualquier número de parámetros y dentro

  • Llamamos al constructor de la clase original pasándole los parámetros que nos pasen
  • Ponemos el log que saldrá cada vez que alguien haga un new.

Añadir métodos y atributos a la clase[editar]

Con el mismo mecanismo, podemos añadir más propiedades y métodos a la clase original sin más que añadirlos a la clase hija.

// Definición de un decorador de clase
function SaludarClase<T extends { new (...args: any[]): {} }>(originalClass: T) {
    console.log("Hola desde el constructor");
    return class extends originalClass {
        constructor(...args: any[]){
            super(args);
            console.log(originalClass); // Saca por pantalla [class NombreClaseQueSeInstancia]
        }
        newMethod(): void {
            console.log("Soy el método nuevo");
        }
        newAttribute: string = "Hola!";
    }
}

Esto sólo tiene una pega, si hacemos un new de la clase original new Ejemplo(), no podemos usar estos nuevos métodos y atributos, ya que la clase original no los tiene. Debemos usar un truco como el siguiente

const ejemplo = new Ejemplo2();

(ejemplo2 as any).newMethod();
(ejemplo2 as any).newAttribute;

Lo cual no es muy elegante.