官术网_书友最值得收藏!

Decorators in TypeScript

Decorators are a very cool functionality, initially proposed by Google in AtScript (a superset of TypeScript that finally got merged into TypeScript back in early 2015). They are a part of the current standard proposition for ECMAScript 7. In a nutshell, decorators are a way to add metadata to class declarations for use by dependency injection or compilation directives. By creating decorators, we are defining special annotations that may have an impact on the way our classes, methods, or functions behave or just simply altering the data we define in fields or parameters. In that sense, they are a powerful way to augment our type's native functionalities without creating subclasses or inheriting from other types. It is, by far, one of the most interesting features of TypeScript. It is extensively used in Angular when designing directives and components or managing dependency injection, as we will learn later in Chapter 4, Enhance Components with Pipes and Directives.

The @ prefix can easily recognize decorators in a name, and they are usually located as standalone statements above the element they decorate.

We can define up to four different types of decorators, depending on what element each type is meant to decorate:

  • Class decorators
  • Property decorators
  • Method decorators
  • Parameter decorators

We'll look as these types of decorators in the following subsections.

Important Note

The Angular framework defines its own decorators, which we are going to use during the development of an application.

Class decorators

Class decorators allow us to augment a class or perform operations over its members. The decorator statement is executed before the class gets instantiated. Creating a class decorator requires defining a plain function, whose signature is a pointer to the constructor belonging to the class we want to decorate, typed as a function (or any other type that inherits from the function). The formal declaration defines a ClassDecorator, as follows:

declare type ClassDecorator = <TFunction extends Function>(Target:TFunction) => TFunction | void;

It's complicated to grasp what this gibberish means, isn't it? Let's put everything in context through a simple example, like this:

function Banana(target: Function): void {

    target.prototype.banana = function(): void {

        console.log('We have bananas!');

    }

}

@Banana

class FruitBasket {

    constructor() {}

}

const basket = new FruitBasket();

basket.banana();

As we can see, we have gained a banana method, which was not originally defined in the FruitBasket class, by properly decorating it with the @Banana decorator. It is worth mentioning, though, that this won't compile. The compiler will complain that FruitBasket does not have a banana method, and rightfully so because TypeScript is typed. So, at this point, we need to tell the compiler that this is valid. So, how do we do that? One way is that, when we create our basket instance, we give it the any type, like so:

const basket: any = new FruitBasket();

Another way of essentially accomplishing the same effect is to type this instead:

const basket = new FruitBasket();

(basket as any).banana();

Here, we are doing a conversion on the fly with the as keyword, and we tell the compiler that this is valid.

Extending a class decorator

Sometimes, we might need to customize the way our decorator operates upon instantiating it. We can design our decorators with custom signatures and then have them returning a function with the same signature we defined when designing class decorators with no parameters. The following piece of code illustrates the same functionality as the previous example, but it allows us to customize the message:

function Banana(message: string) {

    return function(target: Function) {

        target.prototype.banana = function(): void {

            console.log(message);

        }

    }

}

@Banana('Bananas are yellow!')

class FruitBasket {

    constructor() {}

}

As a rule of thumb, decorators that accept parameters require a function whose signature matches the parameters we want to configure and returns another function whose signature matches that of the decorator we want to define.

Property decorators

Property decorators are applied to class fields and are defined by creating a PropertyDecorator function, whose signature takes two parameters:

  • target: The prototype of the class we want to decorate
  • key: The name of the property we want to decorate

Possible use cases for this specific type of decorator consist of logging the values assigned to class fields when instantiating objects of such a class, or when reacting to data changes in such fields. Let's see an actual example that showcases both behaviors:

function Jedi(target: Object, key: string) {

    let propertyValue: string = this[key];

    if (delete this[key]) {

        Object.defineProperty(target, key, {

            get: function() {

                return propertyValue;

            },

            set: function(newValue){

                propertyValue = newValue;

                console.log(`${propertyValue} is a Jedi`);

            }

        });

    }

}

class Character {

    @Jedi

    name: string;

}

const character = new Character();

character.name = 'Luke';

The same logic for parameterized class decorators applies here, although the signature of the returned function is slightly different so that it matches that of the parameterless decorator declaration we saw earlier. The following example depicts how we can log changes on a given class property:

function NameChanger(callbackObject: any): Function {

    return function(target: Object, key: string): void {

        let propertyValue: string = this[key];

        if (delete this[key]) {

            Object.defineProperty(target, key, {

                get: function() {

                    return propertyValue;

                },

                set: function(newValue) {

                    propertyValue = newValue;

                    callbackObject.changeName.call(this,                     propertyValue);

                }

            });

        }

    }

}

class Character {

    @NameChanger ({

        changeName: function(newValue: string): void {

            console.log(`You are now known as ${newValue}`);

        }

    })

    name: string;

}

var character = new Character();

character.name = 'Anakin';

A custom function is triggered upon changing that class property.

Method decorators

This decorator can detect, log, and intervene in terms of how methods are executed. To do so, we need to define a MethodDecorator function whose payload takes the following parameters:

  • target: Represents the decorated method (object).
  • key: The actual name of the decorated method (string).
  • value: This is a property descriptor of the given method. It's a hash object containing, among other things, a property named value with a reference to the method itself.

In the following example, we're creating a decorator that displays how a method is called:

function Log(){

    return function(target, propertyKey: string, descriptor: PropertyDescriptor) {

        const oldMethod = descriptor.value;

        descriptor.value = function newFunc( ...args:any[]){

            let result = oldMethod.apply(this, args);

            console.log(`${propertyKey} is called with ${args.            join(',')} and result ${result}`);

            return result;

        }

    }

}

class Hero {

    @Log()

    attack(...args:[]) { return args.join(); }

}

const hero = new Hero();

hero.attack();

This also illustrates what the arguments were upon calling the method, and what the result of the method's invocation was.

Parameter decorator

Our last decorator covers the ParameterDecorator function, which taps into parameters located in function signatures. This sort of decorator is not intended to alter the parameter information or the function behavior, but to look into the parameter value and perform operations elsewhere, such as logging or replicating data. It accepts the following parameters:

  • target: This is the object prototype where the function, whose parameters are decorated, usually belongs to a class.
  • key: This is the name of the function whose signature contains the decorated parameter.
  • parameterIndex: This is the index in the parameters array where this decorator has been applied.

The following example shows a working example of a parameter decorator:

function Log(target: Function, key: string, parameterIndex: number) {

    const functionLogged = key || target.prototype.constructor.    name;

    console.log(`The parameter in position ${parameterIndex} at     ${functionLogged} has been decorated`);

}

class Greeter {

    greeting: string;

    

    constructor (@Log phrase: string) {

        this.greeting = phrase;

    }

}

You have probably noticed the weird declaration of the functionLogged variable. This is because the value of the target parameter varies, depending on the function whose parameters are decorated. Therefore, it is different if we decorate a constructor parameter or a method parameter. The former returns a reference to the class prototype, while the latter returns a reference to the constructor function. The same applies to the key parameter, which is undefined when decorating the constructor parameters.

Parameter decorators do not modify the value of the parameters decorated or alter the behavior of the methods or constructors where these parameters live. Their purpose is usually to log or prepare the container object for implementing additional layers of abstraction or functionality through higher-level decorators, such as a method or class decorator. Usual case scenarios for this encompass logging component behavior or managing dependency injection.

主站蜘蛛池模板: 禹州市| 来宾市| 浑源县| 贵定县| 汤阴县| 云林县| 襄垣县| 贡山| 二连浩特市| 裕民县| 嘉兴市| 志丹县| 镇雄县| 灌阳县| 宜都市| 平远县| 山东省| 融水| 大荔县| 拜泉县| 达尔| 习水县| 桂平市| 枣阳市| 怀来县| 鄂托克前旗| 海安县| 临夏县| 林周县| 南雄市| 黄山市| 巧家县| 辽阳县| 山丹县| 紫金县| 兰州市| 富锦市| 营山县| 清水河县| 富蕴县| 鞍山市|