Mastering TypeScript 3
上QQ阅读APP看书,第一时间看更新

Using method decorators

Since we have the definition of a function available to us within a method decorator, we could use the decorator to modify the functionality of a specific function. Suppose that we wanted to create an audit trail of some sort, and log a message to the console every time a method was called. This is the perfect scenario for method decorators.

Consider the following method decorator:

function auditLogDec(target: any, 
    methodName: string, 
    descriptor?: PropertyDescriptor) { 
 
    let originalFunction = target[methodName]; 
 
    let auditFunction = function (this: any) { 
        console.log(`auditLogDec : overide of ` 
            + ` ${methodName} called`); 
        for (let i = 0; i < arguments.length; i++) { 
            console.log(`arg : ${i} = ${arguments[i]}`); 
        } 
        originalFunction.apply(this, arguments); 
    } 
 
    target[methodName] = auditFunction; 
    return target; 
}

Here, we have defined a method decorator named auditLogDec. Within this decorator, we are creating a variable named originalFunction to hold the definition of the method that we are decorating, as target[methodName] will return the function definition itself. We then create a new function named auditFunction,with a single parameter named this of type any. The use of the parameter this in this instance is designed to work around one of the strict compiler rules named noImplicitThis. We will discuss these compiler rules in the next chapter.

The first line of the auditFunction function logs a message to the console indicating that the function has indeed been called. We are then iterating through the special JavaScript variable named arguments to log a list of the arguments to the console. Note, however, the last line of the auditFunction function. We are using the JavaScript apply function to call the original function, passing in the this parameter, and the arguments parameter.

After defining the auditFunction function, we then assign it to the original class function through the use of target[methodName]. In essence, we have replaced the original function with our new auditFunction, and, within the auditFunction, have invoked the original function through the apply method.

To show this in action, consider the following class declaration:

class ClassWithAuditDec { 
    @auditLogDec 
    print(arg1: string, arg2: string) { 
        console.log(`ClassWithMethodDec.print` 
            + `(${arg1}, ${arg2}) called.`); 
    } 
} 
 
let auditClass = new ClassWithAuditDec(); 
auditClass.print("test1", "test2");

Here, we are creating a class named ClassWithAuditDec, and decorating the print function with our auditLogDec method decorator. The print function has two arguments, and simply writes a message to the console showing that this function has been called with two arguments. The last two lines of this code snippet are an example of how this class would be used, and will produce the following output:

auditLogDec : overide of  print called
arg : 0 = test1
arg : 1 = test2
ClassWithMethodDec.print(test1, test2) called.

As can be seen in this output, our decorator audit function is being called before the actual implementation of the print function on the class. It is also able to log the values of each argument to the console, and has then invoked the original function correctly. Using decorators in this way is a powerful method of injecting extra functionality non-intrusively into a class declaration. Any class we create can easily include the auditing functionality simply by decorating the relevant class methods.