Comment implémenter un décorateur tapuscrit?

207

TypeScript 1.5 a maintenant des décorateurs .

Quelqu'un pourrait-il fournir un exemple simple démontrant la bonne façon d'implémenter un décorateur et décrire ce que signifient les arguments dans les signatures de décorateur valides possibles?

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;

De plus, y a-t-il des considérations de bonnes pratiques à garder à l'esprit lors de la mise en œuvre d'un décorateur?

David Sherret
la source
Remarque pour moi :-) si vous voulez injecter un @Injectabledans un décorateur, référez
Anand Rockzz
Je suggère de jeter un coup d'œil aux multiples exemples de ce projet. Il existe plusieurs décorateurs - certains sont très simples et certains peuvent être un peu plus difficiles à comprendre: github.com/vlio20/utils-decorators
vlio20

Réponses:

396

J'ai fini par jouer avec les décorateurs et j'ai décidé de documenter ce que j'ai découvert pour quiconque veut en profiter avant que la documentation ne sorte. N'hésitez pas à le modifier si vous voyez des erreurs.

Points généraux

  • Les décorateurs sont appelés lorsque la classe est déclarée, pas lorsqu'un objet est instancié.
  • Plusieurs décorateurs peuvent être définis sur la même classe / propriété / méthode / paramètre.
  • Les décorateurs ne sont pas autorisés sur les constructeurs.

Un décorateur valide doit être:

  1. Affectable à l'un des types de décorateur ( ClassDecorator | PropertyDecorator | MethodDecorator | ParameterDecorator).
  2. Renvoie une valeur (dans le cas de décorateurs de classe et de décorateur de méthode) qui peut être affectée à la valeur décorée.

Référence


Décorateur Accessoire formel

Paramètres de mise en œuvre:

  • target: Le prototype de la classe ( Object).
  • propertyKey: Le nom de la méthode ( string| symbol).
  • descriptor: A TypedPropertyDescriptor- Si vous n'êtes pas familier avec les clés d'un descripteur, je vous recommande de lire à ce sujet dans cette documentation sur Object.defineProperty(c'est le troisième paramètre).

Exemple - sans arguments

Utilisation:

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

La mise en oeuvre:

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

Contribution:

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

Production:

Les arguments de méthode sont: ["testing"]

La valeur de retour est: Message - testing

Remarques:

  • N'utilisez pas la syntaxe des flèches lorsque vous définissez la valeur du descripteur. Le contexte de thisne sera pas celui de l'instance si vous le faites.
  • Il est préférable de modifier le descripteur d'origine que d'écraser le descripteur actuel en renvoyant un nouveau descripteur. Cela vous permet d'utiliser plusieurs décorateurs qui modifient le descripteur sans remplacer ce qu'un autre décorateur a fait. Cela vous permet d'utiliser quelque chose comme @enumerable(false)et @logen même temps (Exemple: mauvais vs bon )
  • Utile : l'argument type de TypedPropertyDescriptorpeut être utilisé pour restreindre les signatures de méthode ( exemple de méthode ) ou les signatures d'accesseur ( exemple d' accesseur ) sur lesquelles le décorateur peut être placé.

Exemple - Avec arguments (Decorator Factory)

Lorsque vous utilisez des arguments, vous devez déclarer une fonction avec les paramètres du décorateur puis renvoyer une fonction avec la signature de l'exemple sans arguments.

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

Décorateur de méthode statique

Similaire à un décorateur de méthode avec quelques différences:

  • Son targetparamètre est la fonction constructeur elle-même et non le prototype.
  • Le descripteur est défini sur la fonction constructeur et non sur le prototype.

Décorateur de classe

@isTestable
class MyClass {}

Paramètre d'implémentation:

  • target: La classe sur laquelle le décorateur est déclaré ( TFunction extends Function).

Exemple d'utilisation : utilisation de l'API de métadonnées pour stocker des informations sur une classe.


Décorateur de propriété

class MyClass {
    @serialize
    name: string;
}

Paramètres de mise en œuvre:

  • target: Le prototype de la classe ( Object).
  • propertyKey: Le nom de la propriété ( string| symbol).

Exemple d'utilisation : création d'un @serialize("serializedName")décorateur et ajout du nom de la propriété à une liste de propriétés à sérialiser.


Décorateur de paramètres

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

Paramètres de mise en œuvre:

  • target: Le prototype de la classe ( Function—il semble Functionne plus fonctionner. Vous devez utiliser anyou Objectici maintenant pour utiliser le décorateur dans n'importe quelle classe. Ou spécifier le (s) type (s) de classe auquel vous souhaitez le restreindre)
  • propertyKey: Le nom de la méthode ( string| symbol).
  • parameterIndex: L'index du paramètre dans la liste des paramètres de la fonction ( number).

Exemple simple

Exemple (s) détaillé (s)

David Sherret
la source
Savez-vous où trouver un exemple de décorateur de paramètres? J'ai essayé d'en implémenter un sans succès github.com/Microsoft/TypeScript/issues/…
Remo H. Jansen
1
@OweRReLoaDeD J'ai ajouté un exemple sous le décorateur de paramètres qui déconnecte simplement ce qui est passé au décorateur. Je ne sais pas si c'est utile cependant. Je ne peux pas penser à un bon exemple pour le moment.
David Sherret
Pour info j'ai collecté et modifié ces informations sur github: github.com/arolson101/typescript-decorators
arolson101
--experimentalDecorators doit être défini pour que cet exemple fonctionne
Trident D'Gao
Je suis un peu confus quant à quoi targetou à quoi prototype of the classet keyfait référence.
Satej S
8

Une chose importante que je ne vois pas dans les autres réponses:

Usine de décorateur

Si nous voulons personnaliser la façon dont un décorateur est appliqué à une déclaration, nous pouvons écrire une fabrique de décorateurs. Une Decorator Factory est simplement une fonction qui renvoie l'expression qui sera appelée par le décorateur lors de l'exécution.

// 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 { ... }

Consultez le chapitre Décorateurs du manuel TypeScript .

Ondra Žižka
la source
4
class Foo {
  @consoleLogger 
  Boo(name:string) { return "Hello, " + name }
}
  • cible: prototype de la classe dans le cas ci-dessus c'est "Foo"
  • propertyKey: nom de la méthode appelée, dans le cas ci-dessus "Boo"
  • descripteur: description de l'objet => contient la propriété value, qui à son tour est la fonction elle-même: fonction (nom) {return 'Bonjour' + nom; }

Vous pouvez implémenter quelque chose qui enregistre chaque appel à la 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
la source
1
Il est difficile de compiler cela avec des paramètres de compilation stricts
PandaWood
En fait, c'est faux et ne peut pas être compilé, il faut des accolades directement après le retour de {value: ...}. Cela peut même être vu à partir d'une source potentielle de votre code - blog.wolksoftware.com/…
PandaWood