Decorators are used to modify a class 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 location for making changes which  is 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

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

Decorator applied to a method has 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" 
*/

Property decorator

Property 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" 
*/

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" 
*/

Decorators in the wild

Here are 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(),  defining unique fields @Unique(['username']) etc.
  • The NPM package class-validator is used for validating properties in an object (used in validations such as income request data, arguments of a function). It has decorators like @IsInt() ,@Min(5) ,@Max(25) to set restrictions on a particular field. Here's an example:
export class Post {
  @Length(10, 20)
  title: string;
  
  text: string;

  @IsInt()
  @Min(0)
  @Max(10)
  rating: number;

  @IsEmail()
  email: string;

  @IsFQDN()
  site: string;

  @IsDate()
  createDate: Date;
}