A method decorator is applied to a method declaration.
Method decorator without arguments
If a method decorator has no arguments e.g. @aDecorator, then the decorator function should be declared as:
function aDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor){}
where:
- target: Either the constructor function of the class for a static method, or the prototype of the class for an instance method.
- propertyKey: The name of the method.
- descriptor: The Property Descriptor for the method.
The PropertyDescriptor in TypeScript is defined as:
interface PropertyDescriptor {
configurable?: boolean;
enumerable?: boolean;
value?: any;
writable?: boolean;
get?(): any;
set?(v: any): void;
}
Also check out JavaScript Property Descriptor tutorial.
Example:
In following example the decorator function makes the target method enumerable so that it can be available in a loop (e.g. in for-in loop):
function Enumerable(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("-- target --");
console.log(target);
console.log("-- proertyKey --");
console.log(propertyKey);
console.log("-- descriptor --");
console.log(descriptor);
//make the method enumerable
descriptor.enumerable = true;
}
class Car {
@Enumerable
run() {
console.log("inside run method...");
}
}
console.log("-- creating instance --");
let car = new Car();
console.log("-- looping --");
for (let key in car) {
console.log("key: " + key);
}
Output-- target -- Car {} -- proertyKey -- run -- descriptor -- { value: [Function: run], writable: true, enumerable: false, configurable: true } -- creating instance -- -- looping -- key: run
The compiled JS file
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
function Enumerable(target, propertyKey, descriptor) {
console.log("-- target --");
console.log(target);
console.log("-- proertyKey --");
console.log(propertyKey);
console.log("-- descriptor --");
console.log(descriptor);
//make the method enumerable
descriptor.enumerable = true;
}
class Car {
run() {
console.log("inside run method...");
}
}
__decorate([
Enumerable
], Car.prototype, "run", null);
console.log("-- creating instance --");
let car = new Car();
console.log("-- looping --");
for (let key in car) {
console.log("key: " + key);
}
Using decorator on a static method
Let's see what will be the difference if we use the decorator on a static method:
function Enumerable2(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("-- target --");
console.log(target);
console.log("-- proertyKey --");
console.log(propertyKey);
console.log("-- descriptor --");
console.log(descriptor);
}
class Car2 {
@Enumerable2
static run() {
console.log("inside run method...");
}
}
Output-- target -- [Function: Car2] -- proertyKey -- run -- descriptor -- { value: [Function: run], writable: true, enumerable: false, configurable: true }
As seen above the 'target' argument is of type Function (the constructor function).
Method decorator with arguments
If a method decorator has arguments then those arguments are passed to the decorator function. The decorator function should return another function which should accept the same arguments as we saw in the last case.
function Enumerable3(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value;
}
}
class Car3 {
@Enumerable3(true)
run() {
console.log("inside run method...");
}
}
console.log("-- creating instance --");
let car3 = new Car3();
console.log("-- looping --");
for (let key in car3) {
console.log("key: " + key);
}
Output-- creating instance -- -- looping -- key: run
Wrapping method
Following example shows how to wrap the original method within another function which can be used as an AOP interceptor.
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
let originalMethod = descriptor.value;
//wrapping the original method
descriptor.value = function (...args: any[]) {
console.log("wrapped function: before invoking " + propertyKey);
let result = originalMethod.apply(this, args);
console.log("wrapped function: after invoking " + propertyKey);
return result;
}
}
class Task {
@log
runTask(arg: any): any {
console.log('runTask invoked, args: '+arg);
return "the task result"
}
}
console.log("-- creating an instance --");
let task = new Task();
console.log("-- invoking Task#runTask --");
let result = task.runTask("task input");
console.log("result: " + result); Output-- creating an instance -- -- invoking Task#runTask -- wrapped function: before invoking runTask runTask invoked, args: task input wrapped function: after invoking runTask result: the task result
Method Wrapping and the Decorator with arguments
function Listener(listener: MethodListener<any>) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
let originalMethod = descriptor.value;
//wrapping the original method
descriptor.value = function (...args: any[]) {
console.log("wrapped function: before invoking " + propertyKey);
let result = originalMethod.apply(this, args);
listener.onMethodInvoked(result);
console.log("wrapped function: after invoking " + propertyKey);
return result;
}
}
}
interface MethodListener<T> {
onMethodInvoked(result: T): void;
}
class MyListener implements MethodListener<any> {
onMethodInvoked(result: any): void {
console.log("MyListener#onMethodInvoked: " + result);
}
}
class TaskRunner {
@Listener(new MyListener())
runTask(taskName: string): any {
console.log("runTask invoked: " + taskName);
return "the task result"
}
}
console.log("-- creating an instance --");
let taskRunner = new TaskRunner();
console.log("-- invoking TaskRunner --");
let output = taskRunner.runTask("task input");
console.log(output);
Output-- creating an instance -- -- invoking TaskRunner -- wrapped function: before invoking runTask runTask invoked: task input MyListener#onMethodInvoked: the task result wrapped function: after invoking runTask the task result
Example ProjectDependencies and Technologies Used: |