Typescript is a superset of Javascript. Typescript allows Javascript developers to add static type-checking and other modern features to plain Javascript. Some of these extra features do get added to the standard Javascript(ECMAScript).
The concept of Decorators is one of those extra features. In this article, we would see what Decorators are and how they can be used in Typescript.
Decorators are used to add special behaviors to a class or to members of a class. The decorator is a function that takes in arguments and adds some annotation or metadata to the arguments in a declarative way.
Decorators are prefixed with an @
. The value after the @
must evaluate to a function that has information about the decorated declaration.
Decorators can be attached to a class declaration, method, property, accessor, or parameter.
Decorators are currently a stage 2 proposal in Javascript, but they are available as an experimental feature in Typescript. To use decorators, we have to set the experimentalDecorators compiler option in the tsconfig.json
file.
Since decorators are functions, we might need to pass in some additional options so as to customize how the decorator works. To do this, we make use of Decorator Factories. A Decorator Factory is simply a function that returns a function. This returned function would then implement the decorator.
const decoratorFactory = (value: string) => {
// the decorator factory returns a decorator function
return (target: any) => {
// the returned decorator uses 'target' and 'value'
}
}
A Class Decorator is used to decorate a class declaration. The class decorator receives the constructor of the class as its only argument. The decorator can be used to observe, modify, or replace a class definition.
const classDecorator = (constructor: Function) => {
// do something with your class
}
@classDecorator
class Person {}
The example below adds a property to the class by returning a new class that extends the constructor passed into the decorator and adds the property.
const addAgeToPerson = <T extends { new (...args: any[]): {} }>(
originalConstructor: T
) => {
return class extends originalConstructor {
age: number;
constructor(...args: any[]) {
super();
this.age = 28;
}
};
};
@addAgeToPerson
class Person {}
const person = new Person();
console.log(person.age); // 28
A Property Decorator is used to decorate a property of a class. The property decorator receives two arguments.
const propertyDecorator = (target: any, propertyName: string) => {
// do something with your property
}
class Person {
@propertyDecorator
age = 28
}
Property decorators can be used to override the property being decorated. This can be done with the static method Object.defineProperty and a new setter and getter for the property.
In the example below, we are going to prevent the age from being changed to a value lower than 18.
const propertyDecorator = (target: any, propertyName: string) => {
let currentAge: number = target[propertyName];
Object.defineProperty(target, propertyName, {
set: (newAge: number) => {
if (newAge < 18) {
return;
}
currentAge = newAge;
},
get: () => currentAge
});
}
class Person {
@propertyDecorator
age = 28
}
const person = new Person();
console.log(person.age); // 28
person.age = 16;
console.log(person.age); // 28
person.age = 24;
console.log(person.age); // 24
An Accessor Decorator is used to decorate an accessor property of a class. The accessor decorator gets applied to the Property Descriptor for that accessor.
Since the Property Descriptor combines both the get
and set
accessor, all decorators for the member must be applied to one accessor, not to both of the accessors.
The accessor decorator receives three arguments.
const accessorDecorator = (target: any, memberName: string, descriptor: PropertyDescriptor) => {
// do something with your accessor
console.log('Accessor decorator!');
console.log(target);
console.log(memberName);
console.log(descriptor);
}
class Product {
title: string;
private _price: number;
@accessorDecorator
get price() {
return this._price
}
set price(val: number) {
if (val > 0) {
this._price = val;
} else {
throw new Error('Price cannot be lower than zero!');
}
}
constructor(t: string, p: number) {
this.title = t;
this._price = p;
}
}
When the above code runs, it produces the output below. As seen from the result, the decorator receives both the set
and get
accessor in the Property Descriptor, despite being added to just one of the accessors.
The value returned from the accessor decorator would be used as the new Property Descriptor for the member.
In the example below, we are using a decorator factory to change the configurable value of the accessor.
const configurable = (value: boolean) => {
return (target: any, memberName: string, descriptor: PropertyDescriptor) => {
descriptor.configurable = value;
}
}
class Product {
title: string;
private _price: number;
@configurable(false)
get price() {
return this._price
}
set price(val: number) {
if (val > 0) {
this._price = val;
} else {
throw new Error('Price cannot be lower than zero!');
}
}
constructor(t: string, p: number) {
this.title = t;
this._price = p;
}
}
A Method Decorator is used to decorate a class method. The method decorator is applied to the Property Descriptor for that method.
The method decorator receives three arguments.
const methodDecorator = (target: any, methodName: string, descriptor: PropertyDescriptor) => {
// do something with your method
console.log('Method decorator!');
console.log(target);
console.log(methodName);
console.log(descriptor);
}
class Product {
title: string;
private _price: number;
constructor(t: string, p: number) {
this.title = t;
this._price = p;
}
@methodDecorator
getPriceWithDiscount(discount: number) {
return this._price - (this._price * discount) / 100;
}
}
When the above code runs, it produces the output below.
The value returned from the accessor decorator would be used as the new Property Descriptor for the member.
In the example below we would use a method decorator to add a deprecated warning to our method.
We do this by returning a wrapper function that wraps around the original method and adds the new functionality. This wrapper function is then placed in a getter function and returned as the new Property Descriptor.
const methodDecorator = (target: any, methodName: string, descriptor: PropertyDescriptor) => {
const originalMethod = descriptor.value;
// new property descriptor
const newDescriptor: PropertyDescriptor = {
configurable: true,
enumerable: false,
// getter function
get() {
// wrapper function
const newMethod = (...args: any[]) => {
console.warn(`Method ${methodName} is deprecated`);
return originalMethod.apply(this, args)
}
return newMethod;
}
};
return newDescriptor;
}
class Product {
title: string;
private _price: number;
constructor(t: string, p: number) {
this.title = t;
this._price = p;
}
@methodDecorator
getPriceWithDiscount(discount: number) {
return this._price - (this._price * discount) / 100;
}
}
const prod = new Product("shoes", 100)
console.log(prod.getPriceWithDiscount(20))
The code above produces the result below.
A Parameter Decorator is is used to decorate a parameter of a class method or a class constructor.
The parameter decorator receives three arguments.
const parameterDecorator = (target: any, methodName: string, position: number) => {
// do something with your parameter
console.log('Parameter decorator!');
console.log(target);
console.log(methodName);
console.log(position);
}
class Product {
title: string;
private _price: number;
constructor(t: string, p: number) {
this.title = t;
this._price = p;
}
getPriceWithDiscount(@parameterDecorator discount: number) {
return this._price - (this._price * discount) / 100;
}
}
When the above code runs, it produces the output below.