Decorators and reflection in TypeScript: from beginner to expert (part 3)

Original author: Remo H. Jansen
  • Transfer

This article is the third part of the series:



Last time, we learned what decorators are and how they are implemented in TypeScript. We know how to work with class, property, and method decorators.


In this article we will talk about:


  • The last remaining type of decorators is the parameter decorator.
  • Decorator Factory Implementation
  • Implementing Configurable Decorators

We will use the following class to demonstrate these concepts:


class Person { 
  public name: string;
  public surname: string;
  constructor(name : string, surname : string) { 
    this.name = name;
    this.surname = surname;
  }
  public saySomething(something : string) : string { 
    return this.name + " " + this.surname + " says: " + something; 
  }
}

Parameter Decorators


As we already know, the signature of the parameter decorator is as follows:


declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;

Using a decorator called logParameterwill look like this:


class Person { 
  public name: string;
  public surname: string;
  constructor(name : string, surname : string) { 
    this.name = name;
    this.surname = surname;
  }
  public saySomething(@logParameter something : string) : string { 
    return this.name + " " + this.surname + " says: " + something; 
  }
}

When compiling in JavaScript, the method is called here __decorate(we talked about it in the first part ).


 Object.defineProperty(Person.prototype, "saySomething",
        __decorate([
            __param(0, logParameter)
        ], Person.prototype, "saySomething", Object.getOwnPropertyDescriptor(Person.prototype, "saySomething")));
    return Person;

By analogy with the previous types of decorators, we can assume that once a method is called Object.defineProperty, the method saySomethingwill be replaced by the result of the function call __decorate(as in the method decorator). This assumption is incorrect.


If you look closely at the code above, you will notice that there is a new function __param. It was generated by the TypeScript compiler and looks like this:


var __param = this.__param || function (index, decorator) {
    // return a decorator function (wrapper)
    return function (target, key) {
        // apply decorator (return is ignored)
        decorator(target, key, index); 
    }
};

The function __paramreturns a decorator that wraps the decorator passed to the input (with a name decorator).


You may notice that when the parameter decorator is called, its value is ignored. This means that when a function is called __decorate, the result of its execution will not override the method saySomething.


Therefore, parameter decorators do not return anything .


Decorator wrapping in is __paramused to save the index (position of the parameter being decorated in the argument list) in the closure.


class foo {
  // foo index === 0
  public foo(@logParameter foo: string) : string { 
    return "bar"; 
  }
  // bar index === 1
  public foobar(foo: string, @logParameter bar: string) : string { 
    return "foobar"; 
  }
}

Now we know that the parameter decorator takes 3 arguments:


  • Prototype of the decorated class
  • Name of the method containing the parameter to decorate
  • Index of decorated parameter

Let's implement logProperty


function logParameter(target: any, key : string, index : number) {
  var metadataKey = `log_${key}_parameters`;
  if (Array.isArray(target[metadataKey])) {
    target[metadataKey].push(index);
  }
  else { 
    target[metadataKey] = [index];
  }
}

The parameter decorator described above adds a new property ( metadataKey) to the class prototype. This property is an array containing the indices of the parameters to be decorated. We can consider this property metadata .


It is assumed that the parameter decorator is not used to modify the behavior of a constructor, method, or property. Parameter decorators should only be used to create various metadata .


Once the metadata is created, we can use another decorator to read it. For example, the following is a modified version of the method decorator from the second part of the article .


The original version displayed in the console the name of the method and all its arguments when called. The new version reads metadata , and on its basis displays only those arguments that are marked with the corresponding parameter decorator.


class Person { 
  public name: string;
  public surname: string;
  constructor(name : string, surname : string) { 
    this.name = name;
    this.surname = surname;
  }
  @logMethod
  public saySomething(@logParameter something : string) : string { 
    return this.name + " " + this.surname + " says: " + something; 
  }
}
function logMethod(target: Function, key: string, descriptor: any) {
  var originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    var metadataKey = `__log_${key}_parameters`;
    var indices = target[metadataKey];
    if (Array.isArray(indices)) { 
      for (var i = 0; i < args.length; i++) { 
        if (indices.indexOf(i) !== -1) { 
          var arg = args[i];
          var argStr = JSON.stringify(arg) || arg.toString();
          console.log(`${key} arg[${i}]: ${argStr}`);
        }
      }
      var result = originalMethod.apply(this, args);
      return result;
    }
    else {
      var a = args.map(a => (JSON.stringify(a) || a.toString())).join();
      var result = originalMethod.apply(this, args);
      var r = JSON.stringify(result);
      console.log(`Call: ${key}(${a}) => ${r}`);
      return result;
    }
  }
  return descriptor;
}

In the next part, we will find out the best way to work with metadata: Metadata Reflection API . Here is a small example of what we will learn:


function logParameter(target: any, key: string, index: number) {
  var indices = Reflect.getMetadata(`log_${key}_parameters`, target, key) || [];
  indices.push(index); 
  Reflect.defineMetadata(`log_${key}_parameters`, indices, target, key);
}

Decorator factory


The official proposal for decorators in TypeScript gives the following definition for a decorator factory:


A decorator factory is a function that can take any number of arguments and returns a decorator of one of the types.

We have already learned how to implement and use all types of decorators (class, method, property and parameter), but we can improve something. Let's say we have a piece of code like this:


@logClass
class Person { 
  @logProperty
  public name: string;
  public surname: string;
  constructor(name : string, surname : string) { 
    this.name = name;
    this.surname = surname;
  }
  @logMethod
  public saySomething(@logParameter something : string) : string { 
    return this.name + " " + this.surname + " says: " + something; 
  }
}

It works as expected, but it would be better if you could use the same decorator everywhere without worrying about its type , as in this example:


@log
class Person { 
  @log
  public name: string;
  public surname: string;
  constructor(name : string, surname : string) { 
    this.name = name;
    this.surname = surname;
  }
  @log
  public saySomething(@log something : string) : string { 
    return this.name + " " + this.surname + " says: " + something; 
  }
}

We can achieve this by wrapping the decorators in the factory. The factory can determine the type of decorator required by the arguments passed to it:


function log(...args : any[]) {
  switch(args.length) {
    case 1:
      return logClass.apply(this, args);
    case 2:
      return logProperty.apply(this, args);
    case 3:
      if(typeof args[2] === "number") {
        return logParameter.apply(this, args);
      }
      return logMethod.apply(this, args);
    default:
      throw new Error("Decorators are not valid here!");
  }
}

Configurable Decorators


The last point that I would like to discuss in this article is how we can pass arguments to the decorator when using it .


@logClassWithArgs({ when : { name : "remo"} })
class Person { 
  public name: string;
  // ...
}

We can use the decorator factory to create configurable decorators:


function logClassWithArgs(filter: Object) {
    return (target: Object) => {
        // реализация декоратора класса будет тут, декоратор
        // будет иметь доступ к параметрам декоратора (filter),
        // потому что они хранятся в замыкании
    }
}

We can apply the same idea to other types of decorators.


Conclusion


Now we have a deep understanding of all four existing types of decorators, how to create decorator factories and how to parameterize them.


In the next article, we will learn how to use the Metadata Reflection API .


Also popular now: