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;
}