How to implement a typescript decorator?


Translate

TypeScript 1.5 now has decorators.

Could someone provide a simple example demonstrating the proper way to implement a decorator and describe what the arguments in the possible valid decorator signatures mean?

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;

Additionally, are there any best practice considerations that should be kept in mind while implementing a decorator?


All Answers
  • Translate

    I ended up playing around with decorators and decided to document what I figured out for anyone who wants to take advantage of this before any documentation comes out. Please feel free to edit this if you see any mistakes.

    General Points

    • Decorators are called when the class is declared—not when an object is instantiated.
    • Multiple decorators can be defined on the same Class/Property/Method/Parameter.
    • Decorators are not allowed on constructors.

    A valid decorator should be:

    1. Assignable to one of the Decorator types (ClassDecorator | PropertyDecorator | MethodDecorator | ParameterDecorator).
    2. Return a value (in the case of class decorators and method decorator) that is assignable to the decorated value.

    Reference


    Method / Formal Accessor Decorator

    Implementation parameters:

    • target: The prototype of the class (Object).
    • propertyKey: The name of the method (string | symbol).
    • descriptor: A TypedPropertyDescriptor — If you're unfamiliar with a descriptor's keys, I would recommend reading about it in this documentation on Object.defineProperty (it's the third parameter).

    Example - Without Arguments

    Use:

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

    Implementation:

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

    Input:

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

    Output:

    The method args are: ["testing"]

    The return value is: Message -- testing

    Notes:

    • Do not use arrow syntax when setting the descriptor's value. The context of this will not be the instance's if you do.
    • It's better to modify the original descriptor than overwriting the current one by returning a new descriptor. This allows you to use multiple decorators that edit the descriptor without overwriting what another decorator did. Doing this allows you to use something like @enumerable(false) and @log at the same time (Example: Bad vs Good)
    • Useful: The type argument of TypedPropertyDescriptor can be used to restrict what method signatures (Method Example) or accessor signatures (Accessor Example) the decorator can be put on.

    Example - With Arguments (Decorator Factory)

    When using arguments, you must declare a function with the decorator's parameters then return a function with the signature of the example without 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;
        };
    }
    

    Static Method Decorator

    Similar to a method decorator with some differences:

    • Its target parameter is the constructor function itself and not the prototype.
    • The descriptor is defined on the constructor function and not the prototype.

    Class Decorator

    @isTestable
    class MyClass {}
    

    Implementation parameter:

    • target: The class the decorator is declared on (TFunction extends Function).

    Example use: Using the metadata api to store information on a class.


    Property Decorator

    class MyClass {
        @serialize
        name: string;
    }
    

    Implementation parameters:

    • target: The prototype of the class (Object).
    • propertyKey: The name of the property (string | symbol).

    Example use: Creating a @serialize("serializedName") decorator and adding the property name to a list of properties to serialize.


    Parameter Decorator

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

    Implementation parameters:

    • target: The prototype of the class (Function—it seems Function doesn't work anymore. You should use any or Object here now in order to use the decorator within any class. Or specify the class type(s) you want to restrict it to)
    • propertyKey: The name of the method (string | symbol).
    • parameterIndex: The index of parameter in the list of the function's parameters (number).

    Simple example

    Detailed Example(s)


  • Translate

    One important thing I don't see in the other answers:

    Decorator factory

    If we want to customize how a decorator is applied to a declaration, we can write a decorator factory. A Decorator Factory is simply a function that returns the expression that will be called by the decorator at runtime.

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

    Check the TypeScript handbook Decorators chapter.


  • Translate
    class Foo {
      @consoleLogger 
      Boo(name:string) { return "Hello, " + name }
    }
    
    • target: prototype of the class in the above case it's "Foo"
    • propertyKey: name of the method called, in the above case "Boo"
    • descriptor: description of object => contains value property, which in turn is the function itself: function(name) { return 'Hello' + name; }

    You could implement something that logs each call to the 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;
      }     
    }