DEV Community

Shakil Ahmed
Shakil Ahmed

Posted on • Originally published at shakil.me on

Understanding TypeScript Decorators

Understanding TypeScript Decorators

Decorators are used to modify classes and its members (methods, properties).

The question may be, why do we need decorators to change those things? Why not directly change in the declarations?

Directly changing is not scalable. If you need the same changes in 100 classes, you have to write it 100 times. Using decorators, you write it once and apply it as many times as you want.

Decorators give you a central point of change which easier to work with. Frameworks like NestJS, Angular use decorators extensively.

In TypeScript, you can decorate the following things:

  • class
  • method
  • property
  • accessor
  • parameter

Decorators are a TypeScript feature and are not officially included in JavaScript yet. However, they are likely to be added in the future (Currently in stage 2).

Class decorator

// The following decorator makes any class more Funky
function makeFunky(target: Function) {
    return class extends target {
        constructor() {
            super()
        }
        funkIt() {
            console.log("Get The Funk Outta My Face")
            }
        }
    }
}

@makeFunky
class Car {
    wheels = 4
    state = 'pause'
    drive() {
        this.state = 'running'
    }
}

const car = new Car()
car.funkIt()
// logs: Get The Funk Outta My Face
Enter fullscreen mode Exit fullscreen mode

Decorators are just functions, makeFunky is the decorator above. makeFunky gets the class Car it's applied to as a parameter. It returns a new class which is just a modified version of the original class.

Decorators are called when the class is declared—not when an object is instantiated.

Method decorator

Decorators applied to a method gets the following parameters

  • target : for static methods, target is the constructor function of the class. For an instance method, target is the prototype of the class.
  • propertyKey : The name of the method.
  • descriptor : The Property Descriptor for the method
function log(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value
    descriptor.value = function () {
        console.log('arguments are ', arguments)
        const result = originalMethod.apply(this, arguments)
        console.log(`result is ${result}`)
        return result
    }
}

class Calculator {
    @log
    add(x: number, y: number): number {
        return x + y
    }
}

const calc = new Calculator()
calc.add(10, 20)

/*
[LOG]: "arguments are ",  {
  "0": 10,
  "1": 20
} 
[LOG]: "result is 30" 
*/
Enter fullscreen mode Exit fullscreen mode

Property decorator

Similar to method decorator. Gets the same parameters target, propertyKey, and descriptor as the method decorators. Its return value is ignored.

function logMutation(target: any, key: string ) {
    var _val = target[key]
    Object.defineProperty(target, key, {
        set: function(newVal) {
            console.log('new value is ' + newVal)
            _val = newVal
        },
        get: function() {
                return _val
            }
        })
}

class Person {
    @logMutation
    public age: number

    constructor(age: number) {
        this.age = age
    }
}

const jack = new Person(20)
jack.age = 40

/*
[LOG]: "new value is 20" 
[LOG]: "new value is 40" 
*/
Enter fullscreen mode Exit fullscreen mode

Accessor and parameter decorators

Accessor decorators are same as method decorators, but they are applied to either the getter or setter method of a single member.

Parameter decorators are applied on, you got it, parameters.

Decorator Factory

A common pattern in decorator usage is calling a function that returns a decorator. Here's the previously mentioned method decorator log , returned by invoking the function logger

function logger(functionName: string) {
    function log(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value
        descriptor.value = function () {
            console.log('method name: ' + functionName)
            console.log('arguments are ', arguments)
            const result = originalMethod.apply(this, arguments)
            console.log(`result is ${result}`)
            return result
        }
    }
    return log
}

class Calculator {
    @logger('add')
    add(x: number, y: number): number {
        return x + y
    }
}

const calc = new Calculator()
calc.add(10, 20)

/*
[LOG]: "method name: add" 
[LOG]: "arguments are ", {
  "0": 10,
  "1": 20
} 
[LOG]: "result is 30" 
*/
Enter fullscreen mode Exit fullscreen mode

Decorator in the wild

Some use cases of decorators as being utilized by popular libraries/frameworks.

  • NestJS uses the class decorator @Controller('pathName') to define a class as a controller. Decorators associate classes with required metadata and enable Nest to create a routing map (tie requests to the corresponding controllers). It also uses decorators to define modules, injectable instances for its dependency injection system.
  • TypeORM uses decorators to define a class as an entity @Entity(), tagging properties as columns @Column(), auto-incremented ID etc.
  • The NPM package class-validator is used for validating properties in an object (used in validations such as income data, arguments of a function). It has decorators like @IsInt() @Min(5) @Max(25) to set restrictions on a particular field.

Top comments (0)