The Ultimate guide to Decorators in Typescript
Decorators mean better edge of your TS Sword
What are Decorators
Decorators are a powerful feature in TypeScript that allows developers to modify or extend the behaviour of classes, methods, accessors, and properties. They offer an elegant way to add functionality or modify the behaviour of existing constructs without altering their original implementation.
More formally -
A Decorator is a special kind of declaration that can be attached to a class declaration, method, accessor, property, or parameter. Decorators use the form @expression
, where the expression
must evaluate to a function that will be called at runtime with information about the decorated declaration.
The History of Decorators
Decorators have a rich history in the TypeScript and JavaScript ecosystems. The concept of decorators was inspired by Python and other programming languages. The initial decorator's proposal for JavaScript was introduced in 2014, and since then, several versions of the proposal have been developed, with the current one being at stage 3 of the ECMAScript standardization process which is now officially supported by Typescript 5.0.
To enable experimental support for decorators, you must enable the experimentalDecorators
compiler option either on the command line or in your tsconfig.json
:
Command Line:
tsc --target ES5 --experimentalDecorators
tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
Decorator Functions and Their Capabilities
A decorator is a function that takes the construct being decorated as its argument and may return a modified version of the construct or a new construct altogether. Decorators can be used to:
Modify the behaviour of a class, method, accessor, or property
Add new functionality to a class or method
Provide metadata for a construct
Enforce coding standards or best practices
Decorator Factories
If we want to customize how a decorator is applied to a declaration, we can write a decorator factory. A Decorator Factory is simply a function that returns the expression that will be called by the decorator at runtime.
We can write a decorator factory in the following fashion:
function color(value: string) {
// this is the decorator factory, it sets up
// the returned decorator function
return function (target) {
// this is the decorator
// do something with 'target' and 'value'...
};
}
Decorator Evaluation
There is a well-defined order to how decorators applied to various declarations inside of a class are applied:
Parameter Decorators, followed by Method, Accessor, or Property Decorators are applied for each instance member.
Parameter Decorators, followed by Method, Accessor, or Property Decorators are applied for each static member.
Parameter Decorators are applied for the constructor.
Class Decorators are applied for the class.
Types of Decorators
Decorators can be categorised into the following types -
Class Decorators
Method Decorators
Getter and Setter (Property) Decorators
Field (Parameter) Decorators
Auto-accessor Decorators
Class Decorators
Class decorators are applied to class constructors and can be used to modify or extend the behaviour of a class. Some common use cases for class decorators include:
Collecting instances of a class
Freezing instances of a class
Making classes function-callable
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t; // cannot perform this action now.
}
}
Method Decorators
Method decorators are applied to class methods and can be used to modify or extend the behaviour of a method. Some common use cases for method decorators include:
Tracing method invocations
Binding methods to instances
Applying functions to methods
function Enumerable(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log(target);
console.log("-- proertyKey -- ", propertyKey);
console.log("-- descriptor -- ", 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);
}
Getter and Setter Decorators
Getter and setter decorators are applied to class accessors, allowing developers to modify or extend their behaviour. Common use cases for getter and setter decorators include:
Compute values lazily and cache
Implementing read-only properties
Validating property assignments
function lazy(target: (this: This) => Return, context: ClassGetterDecoratorContext) {
return function (this: This): Return {
const value = target.call(this);
Object.defineProperty(this, context.name, { value, enumerable: true });
return value;
};
}
class MyClass {
private _expensiveValue: number | null = null;
@lazy
get expensiveValue(): number {
this._expensiveValue ??= computeExpensiveValue();
return this._expensiveValue;
}
}
function computeExpensiveValue(): number {
// Expensive computation here…
console.log('computing...'); // Only call once
return 42;
}
const obj = new MyClass();
console.log(obj.expensiveValue);
console.log(obj.expensiveValue);
console.log(obj.expensiveValue);
Field Decorators
Field decorators are applied to class fields and can be used to modify or extend the behaviour of a field. Common use cases for field decorators include:
Changing initialization values of fields
Implementing read-only fields
Dependency injection
Emulating enums
function addOne<T>(target: undefined, context: ClassFieldDecoratorContext<T, number>) {
return function (this: T, value: number) {
console.log('addOne: ', value); // 3
return value + 1;
};
}
function addTwo<T>(
target: undefined,
context: ClassFieldDecoratorContext<T, number>
) {
return function (this: T, value: number) {
console.log('addTwo: ', value); // 1
return value + 2;
};
}
class MyClass {
@addOne
@addTwo
x = 1;
}
console.log(new MyClass().x); // 4
Note: multiple decorators are evaluated in the bottom to the top order.
Auto-Accessor Decorators
Auto-accessors are a new language feature that simplifies the creation of getter and setter pairs. It helps avoid problems that occur when decorator authors try to replace instance fields with accessors on the prototype, because ECMAScript instance fields shadow accessors when they are mounted on the instance.
function logged(value, context) {
const { kind, name } = context;
const fieldName = String(name);
if (kind === "accessor") {
let { get, set } = value;
return {
get(this) {
console.log(`Getting ${fieldName}`);
return get.call(this);
},
set(this, val) {
console.log(`Setting ${fieldName} to ${val}`);
return set.call(this, val);
},
init(initialValue) {
console.log(`Initializing ${fieldName} with value ${initialValue}`);
return initialValue;
},
};
}
}
class User {
@logged
accessor name: string = "Anonymous";
constructor(name: string) {
this.name = name;
}
}
const user = new User("John Doe");
console.log(user.name);
Conclusion
Decorators are a powerful feature in TypeScript that can help you to write better, and compact code.
As of now, Decorators are a Stage 2 proposal for JavaScript and a Phase 3 proposal in Typescript. As the decorator's proposal continues to evolve and mature, you can gradually choose to apply it to your project.
You can find more information at - https://github.com/microsoft/TypeScript/pull/50820