Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement a typescript decorator?

People also ask

Why decorators are used in TypeScript?

Decorators provide a way to add both annotations and a meta-programming syntax for class declarations and members. Decorators are a stage 2 proposal for JavaScript and are available as an experimental feature of TypeScript. NOTE Decorators are an experimental feature that may change in future releases.

How do you make a decorator?

To create a decorator function in Python, I create an outer function that takes a function as an argument. There is also an inner function that wraps around the decorated function. To use a decorator ,you attach it to a function like you see in the code below.

Are TypeScript decorators experimental?

Since decorators are an experimental feature, they are disabled by default. You must enable them by either enabling it in the tsconfig. json or passing it to the TypeScript compiler ( tsc ).

Where can decorators be applied to in Angular?

Class decorators, such as @Component and @NgModule. Property decorators for properties inside classes, such as @Input and @Output. Method decorators for methods inside classes, such as @HostListener. Parameter decorators for parameters inside class constructors, such as @Inject.


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)

  • Memoize decorator - Method, Get/Set Accessor decorator example

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.


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

TS decorators:

TS decorators allow extra functionality to be added on a class. The class is altered by decorators at declare time, before any instance of the class is created.

Syntax:

Decorators are declared with an @ sign, for example @metadata. TS will now search for a corresponding metadata function and will automatically supply it with sevaral argument which vary on what is exactly decorated (e.g. class or class property get different arguments)

These parameters are supplied in the decorator function:

  • The prototype object of the class
  • propertykey or method name
  • PropertyDescriptor object, looks like this {writable: true, enumerable: false, configurable: true, value: ƒ}

Depending on the type of decorator 1-3 of these arguments are passed to the decorator function.

Types of decorators:

The following decorators can be applied to a class and TS will evaluate them in the following order (following summation comes from TS docs):

  1. Parameter Decorators, followed by Method, Accessor, or Property Decorators are applied for each instance member.
  2. Parameter Decorators, followed by Method, Accessor, or Property Decorators are applied for each static member.
  3. Parameter Decorators are applied for the constructor.
  4. Class Decorators are applied for the class

The best way to understand them better is via examples. Note that these example do need significant understanding of the TS language and concepts like PropertyDescriptor.

Method decorators:

function overwrite(
    target: myClass,
    propertyKey: string,
    descriptor: PropertyDescriptor
) {
    console.log('I get logged when the class is declared!')

    // desciptor.value refers to the actual function fo the class
    // we are changing it to another function which straight up 
    // overrides the other function
    descriptor.value = function () {
        return 'newValue method overwritten'
    }
}

function enhance(
    target: myClass,
    propertyKey: string,
    descriptor: PropertyDescriptor
) {
    const oldFunc = descriptor.value;

    // desciptor.value refers to the actual function fo the class
    // we are changing it to another function which calls the old
    // function and does some extra stuff
    descriptor.value = function (...args: any[]) {
        console.log('log before');
        const returnValue = oldFunc.apply(this, args)
        console.log('log after');

        return returnValue;
    }
}


class myClass {

    // here is the decorator applied
    @overwrite
    foo() {
        return 'oldValue';
    }

    // here is the decorator applied
    @enhance
    bar() {
        return 'oldValueBar';
    }

}

const instance =new myClass()

console.log(instance.foo())
console.log(instance.bar())

// The following gets logged in this order:

//I get logged when the class is declared!
// newValue method overwritten
// log before
// log after
// oldValueBar

Property decorators:

function metaData(
    target: myClass,
    propertyKey: string,
    // A Property Descriptor is not provided as an argument to a property decorator due to 
    // how property decorators are initialized in TypeScript.
) {

    console.log('Execute your custom code here')
    console.log(propertyKey)

}

class myClass {

    @metaData
    foo = 5

}


// The following gets logged in this order:

// Execute your custom code here
// foo

Class decorators (from TS docs):

function seal(
    constructor: Function,
) {

    // Object.seal() does the following:
    // Prevents the modification of attributes of 
    // existing properties, and prevents the addition 
    // of new properties
    Object.seal(constructor);
    Object.seal(constructor.prototype);

}

@seal
class myClass {

    bar?: any;
    
    foo = 5

}
 
myClass.prototype.bar = 10;

// The following error will be thrown:

// Uncaught TypeError: Cannot add property bar,
// object is not extensible
 

Decorators and decorator factories:

decorators can be declared via decorators function or decorator factory functions. There is a difference in syntax which is best explained via an example:

// Returns a decorator function, we can return any function
// based on argument if we want
function decoratorFactory(arg: string) {
    return function decorator(
    target: myClass,
    propertyKey: string,
) {
    console.log(`Log arg ${arg} in decorator factory`);
}
}

// Define a decorator function directly
function decorator(
    target: myClass,
    propertyKey: string,
) {
    console.log('Standard argument');
}

class myClass {

    // Note the parentheses and optional arguments 
    // in the decorator factory
    @decoratorFactory('myArgument')
    foo = 'foo';

    // No parentheses or arguments
    @decorator
    bar = 'bar';

}


// The following gets logged in this order:

// Log arg myArgument in decorator factory
// Standard argument