Como implementar um decorador datilografado?

207

O TypeScript 1.5 agora possui decoradores .

Alguém poderia fornecer um exemplo simples demonstrando a maneira correta de implementar um decorador e descrever o que significam os argumentos nas possíveis assinaturas válidas do decorador?

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;

Além disso, existem considerações sobre as melhores práticas que devem ser lembradas ao implementar um decorador?

David Sherret
fonte
Nota para mim mesmo :-) se você quiser injetar um @Injectableem um decorador, consulte #
Anand Rockzz
Eu sugeriria dar uma olhada nos vários exemplos que este projeto possui. Existem vários decoradores - alguns são muito simples e outros podem ser um pouco mais difíceis de entender: github.com/vlio20/utils-decorators
vlio20

Respostas:

396

Acabei brincando com os decoradores e decidi documentar o que descobri para quem quiser tirar vantagem disso antes que a documentação seja publicada. Sinta-se à vontade para editar isso se houver algum erro.

Pontos Gerais

  • Decoradores são chamados quando a classe é declarada - não quando um objeto é instanciado.
  • Vários decoradores podem ser definidos na mesma classe / propriedade / método / parâmetro.
  • Decoradores não são permitidos em construtores.

Um decorador válido deve ser:

  1. Atribuível a um dos tipos de Decorador ( ClassDecorator | PropertyDecorator | MethodDecorator | ParameterDecorator).
  2. Retorne um valor (no caso de decoradores de classe e decorador de método) que é atribuível ao valor decorado.

Referência


Método / Decorador formal de acessador

Parâmetros de implementação:

  • target: O protótipo da classe ( Object).
  • propertyKey: O nome do método ( string| symbol).
  • descriptor: A TypedPropertyDescriptor- Se você não estiver familiarizado com as chaves de um descritor, recomendo ler sobre isso nesta documentação em Object.defineProperty(é o terceiro parâmetro).

Exemplo - sem argumentos

Usar:

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

Implementação:

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");

Resultado:

O método args são: ["testing"]

O valor de retorno é: Mensagem - teste

Notas:

  • Não use a sintaxe da seta ao definir o valor do descritor. O contexto de thisnão será da instância se você o fizer.
  • É melhor modificar o descritor original do que substituir o atual, retornando um novo descritor. Isso permite que você use vários decoradores que editam o descritor sem substituir o que outro decorador fez. Isso permite que você use algo como @enumerable(false)e @logao mesmo tempo (Exemplo: Ruim vs Bom )
  • Útil : O argumento de tipo de TypedPropertyDescriptorpode ser usado para restringir quais assinaturas de método ( Exemplo de Método ) ou assinantes de acessador ( Exemplo de Acessor ) no qual o decorador pode ser colocado.

Exemplo - Com argumentos (Decorator Factory)

Ao usar argumentos, você deve declarar uma função com os parâmetros do decorador e retornar uma função com a assinatura do exemplo sem 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étodo estático

Semelhante a um decorador de métodos com algumas diferenças:

  • Está target parâmetro é a própria função construtora e não o protótipo.
  • O descritor é definido na função construtora e não no protótipo.

Class Decorator

@isTestable
class MyClass {}

Parâmetro de implementação:

  • target: A classe em que o decorador é declarado ( TFunction extends Function).

Exemplo de uso : usando a API de metadados para armazenar informações em uma classe.


Decorador de propriedades

class MyClass {
    @serialize
    name: string;
}

Parâmetros de implementação:

  • target: O protótipo da classe (Object).
  • propertyKey: O nome da propriedade ( string| symbol).

Exemplo de uso : Criando um @serialize("serializedName")decorador e adicionando o nome da propriedade a uma lista de propriedades para serializar.


Decorador de Parâmetros

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

Parâmetros de implementação:

  • target: O protótipo da classe ( Function- parece Functionque não funciona mais. Você deve usar anyou Objectaqui agora para usar o decorador em qualquer classe. Ou especificar o (s) tipo (s) de classe ao qual você deseja restringir)
  • propertyKey: O nome do método ( string| symbol).
  • parameterIndex: O índice do parâmetro na lista de parâmetros da função ( number).

Exemplo simples

Exemplo (s) Detalhado (s)

David Sherret
fonte
Você sabe onde encontrar um exemplo de Parameter Decorator? Eu tenho tentado implementar um sem sucesso github.com/Microsoft/TypeScript/issues/…
Remo H. Jansen
1
@OweRReLoaDeD Adicionei um exemplo no decorador de parâmetros que apenas registra o que é passado para o decorador. Não tenho certeza se isso é útil. Não consigo pensar em um bom exemplo no momento.
David Sherret
Para sua informação, coletei e aprimorei essas informações no github: github.com/arolson101/typescript-decorators
arolson101
--experimentalDecorators bandeira tem que ser ajustado para que este exemplo funcione
Trident D'Gao
Estou um pouco confuso sobre o que targetou o prototype of the classe keyse refere, alguém poderia por favor elaborar sobre isso?
Satej S
8

Uma coisa importante que não vejo nas outras respostas:

Fábrica de decorador

Se quisermos personalizar como um decorador é aplicado a uma declaração, podemos escrever uma fábrica de decoradores. Uma Decorator Factory é simplesmente uma função que retorna a expressão que será chamada pelo decorador em tempo de execução.

// 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 o capítulo Decoradores do manual do TypeScript .

Ondra Žižka
fonte
4
class Foo {
  @consoleLogger 
  Boo(name:string) { return "Hello, " + name }
}
  • target: protótipo da classe no caso acima é "Foo"
  • propertyKey: nome do método chamado, no caso acima "Boo"
  • descritor: description of object => contém a propriedade value, que por sua vez é a própria função: function (name) {return 'Hello' + name; }

Você pode implementar algo que registra cada chamada no console:

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
fonte
1
É uma tarefa difícil conseguir isso para compilar com configurações do compilador rigorosas
PandaWood
De fato, isso está errado e não pode ser compilado; é preciso usar chaves diretamente após o retorno {value: ...}. Isso pode até ser visto a partir de uma fonte potencial de seu código - blog.wolksoftware.com/…
PandaWood 6/17