Close

TypeScript - Method Decorator

[Last Updated: Mar 19, 2019]

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:

  1. target: Either the constructor function of the class for a static method, or the prototype of the class for an instance method.
  2. propertyKey: The name of the method.
  3. 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 Project

Dependencies and Technologies Used:

  • Typescript 3.3.3333
Typescript Method decorator examples Select All Download
  • typescript-method-decorator-examples
    • Ex1MethodDecorator.ts

    See Also