¿Cómo implementar un decorador mecanografiado?

207

TypeScript 1.5 ahora tiene decoradores .

¿Podría alguien proporcionar un ejemplo simple que demuestre la forma correcta de implementar un decorador y describir lo que significan los argumentos en las posibles firmas de decorador válidas?

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: Function, propertyKey: string | symbol, parameterIndex: number) => void;

Además, ¿hay algunas consideraciones de mejores prácticas que se deben tener en cuenta al implementar un decorador?

David Sherret
fuente
Nota para mí :-) si desea inyectar un @Injectableen un decorador, consulte
Anand Rockzz
Sugeriría echar un vistazo a los múltiples ejemplos que tiene este proyecto. Hay varios decoradores, algunos son muy simples y otros pueden ser un poco más difíciles de entender: github.com/vlio20/utils-decorators
vlio20

Respuestas:

396

Terminé jugando con decoradores y decidí documentar lo que descubrí para cualquiera que quiera aprovechar esto antes de que salga la documentación. Siéntase libre de editar esto si ve algún error.

Puntos generales

  • Se llama a los decoradores cuando se declara la clase, no cuando se instancia un objeto.
  • Se pueden definir múltiples decoradores en la misma clase / propiedad / método / parámetro.
  • No se permiten decoradores en los constructores.

Un decorador válido debe ser:

  1. Asignable a uno de los tipos de Decorador ( ClassDecorator | PropertyDecorator | MethodDecorator | ParameterDecorator).
  2. Devuelve un valor (en el caso de decoradores de clase y decorador de métodos) que se puede asignar al valor decorado.

Referencia


Método / Decorador de accesorios formal

Parámetros de implementación:

  • target: El prototipo de la clase ( Object).
  • propertyKey: El nombre del método ( string| symbol).
  • descriptor: A TypedPropertyDescriptor- Si no está familiarizado con las claves de un descriptor, recomendaría leerlo en esta documentación sobre Object.defineProperty(es el tercer parámetro).

Ejemplo: sin argumentos

Utilizar:

class MyClass {
    @log
    myMethod(arg: string) { 
        return "Message -- " + arg;
    }
}

Implementación:

function log(target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<any>) {
    const originalMethod = descriptor.value; // save a reference to the original method

    // NOTE: Do not use arrow syntax here. Use a function expression in 
    // order to use the correct value of `this` in this method (see notes below)
    descriptor.value = function(...args: any[]) {
        // pre
        console.log("The method args are: " + JSON.stringify(args));
        // run and store result
        const result = originalMethod.apply(this, args);
        // post
        console.log("The return value is: " + result);
        // return the result of the original method (or modify it before returning)
        return result;
    };

    return descriptor;
}

Entrada:

new MyClass().myMethod("testing");

Salida:

Los argumentos del método son: ["prueba"]

El valor de retorno es: Mensaje: prueba

Notas:

  • No utilice la sintaxis de flecha al configurar el valor del descriptor. El contexto de thisno será la instancia si lo hace.
  • Es mejor modificar el descriptor original que sobrescribir el actual devolviendo un nuevo descriptor. Esto le permite usar múltiples decoradores que editan el descriptor sin sobrescribir lo que hizo otro decorador. Hacer esto le permite usar algo como @enumerable(false)y @logal mismo tiempo (Ejemplo: Malo vs Bueno )
  • Útil : El argumento tipo de TypedPropertyDescriptorse puede usar para restringir las firmas de métodos ( Ejemplo de método ) o las firmas de acceso ( Ejemplo de acceso ) en que se puede poner el decorador.

Ejemplo - Con argumentos (Fábrica de decoradores)

Al usar argumentos, debe declarar una función con los parámetros del decorador y luego devolver una función con la firma del ejemplo sin argumentos.

class MyClass {
    @enumerable(false)
    get prop() {
        return true;
    }
}

function enumerable(isEnumerable: boolean) {
    return (target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<any>) => {
        descriptor.enumerable = isEnumerable;
        return descriptor;
    };
}

Decorador de métodos estáticos

Similar a un decorador de métodos con algunas diferencias:

  • Su targetparámetro es la función constructora en sí y no el prototipo.
  • El descriptor se define en la función constructora y no en el prototipo.

Decorador de clase

@isTestable
class MyClass {}

Parámetro de implementación:

  • target: La clase en la que se declara el decorador ( TFunction extends Function).

Ejemplo de uso : uso de la API de metadatos para almacenar información en una clase.


Decorador de propiedades

class MyClass {
    @serialize
    name: string;
}

Parámetros de implementación:

  • target: El prototipo de la clase ( Object).
  • propertyKey: El nombre de la propiedad ( string| symbol).

Ejemplo de uso : crear un @serialize("serializedName")decorador y agregar el nombre de la propiedad a una lista de propiedades para serializar.


Decorador de parámetros

class MyClass {
    myMethod(@myDecorator myParameter: string) {}
}

Parámetros de implementación:

  • target: El prototipo de la clase ( Function- parece que Functionya no funciona. Debería usar anyo Objectaquí ahora para usar el decorador dentro de cualquier clase. O especificar el tipo de clase a la que desea restringirlo)
  • propertyKey: El nombre del método ( string| symbol).
  • parameterIndex: El índice del parámetro en la lista de parámetros de la función ( number).

Ejemplo simple

Ejemplo (s) detallado (s)

David Sherret
fuente
¿Sabes dónde encontrar un ejemplo de Decorador de parámetros? He estado tratando de implementar uno sin éxito github.com/Microsoft/TypeScript/issues/…
Remo H. Jansen
1
@OweRReLoaDeD Agregué un ejemplo debajo del decorador de parámetros que simplemente cierra la sesión de lo que se pasa al decorador. Sin embargo, no estoy seguro de si eso es útil. No puedo pensar en un buen ejemplo en este momento.
David Sherret
Para su información, recopilé y modifiqué esta información en github: github.com/arolson101/typescript-decorators
arolson101
- para que este ejemplo funcione
Trident D'Gao
Estoy un poco confundido en cuanto a qué targeto al prototype of the classy se keyrefiere, ¿podría alguien explicarme eso?
Satej S
8

Una cosa importante que no veo en las otras respuestas:

Fábrica de decoradores

Si queremos personalizar cómo se aplica un decorador a una declaración, podemos escribir una fábrica de decoradores. Una fábrica de decoradores es simplemente una función que devuelve la expresión que el decorador invocará en tiempo de ejecución.

// This is a factory, returns one of ClassDecorator,
// PropertyDecorator, MethodDecorator, ParameterDecorator
function Entity(discriminator: string):  {
    return function(target) {
        // this is the decorator, in this case ClassDecorator.
    }
}

@Entity("cust")
export class MyCustomer { ... }

Consulte el capítulo Decoradores del manual de TypeScript .

Ondra Žižka
fuente
4
class Foo {
  @consoleLogger 
  Boo(name:string) { return "Hello, " + name }
}
  • target: prototipo de la clase en el caso anterior es "Foo"
  • propertyKey: nombre del método llamado, en el caso anterior "Boo"
  • descriptor: la descripción del objeto => contiene la propiedad de valor, que a su vez es la función en sí misma: function (name) {return 'Hello' + name; }

Podría implementar algo que registre cada llamada a la consola:

function consoleLogger(target: Function, key:string, value:any) 
{
  return value: (...args: any[]) => 
  {
     var a = args.map(a => JSON.stringify(a)).join();
     var result = value.value.apply(this, args);
     var r = JSON.stringify(result);

     console.log('called method' + key + ' with args ' + a + ' returned result ' + r);

     return result;
  }     
}
Erik Lieben
fuente
1
Conseguir
De hecho, esto está mal y no se puede compilar, es necesario que se coloquen llaves directamente después de devolver {valor: ...}. Esto incluso se puede ver desde una fuente potencial de su código - blog.wolksoftware.com/…
PandaWood