TypeScript Decorators: What
In the world of TypeScript, decorators are a powerful feature that allows you to add metadata and behavior to classes, methods, accessors, properties, or parameters at design time. They are a way to extend the functionality of existing code without modifying its core structure. Decorators are a form of metaprogramming, enabling developers to write more modular, reusable, and maintainable code. This blog post will explore the core concepts of TypeScript decorators, their typical usage scenarios, and common best practices.
Table of Contents
- Core Concepts of TypeScript Decorators
- What are Decorators?
- Decorator Factories
- Decorator Composition
- Typical Usage Scenarios
- Class Decorators
- Method Decorators
- Property Decorators
- Parameter Decorators
- Common Best Practices
- Using Decorators for Logging
- Validation with Decorators
- Dependency Injection with Decorators
- Conclusion
- FAQ
- References
Detailed and Structured Article
Core Concepts of TypeScript Decorators
What are Decorators?
A decorator in TypeScript is a special kind of declaration that can be attached to a class declaration, method, accessor, property, or parameter. It uses the form @expression, where expression must evaluate to a function that will be called at runtime with information about the decorated declaration.
Here is a simple example of a class decorator:
function logClass(constructor: Function) {
console.log(`Class ${constructor.name} was created.`);
}
@logClass
class MyClass {
constructor() {
console.log('Inside MyClass constructor');
}
}
const instance = new MyClass();
In this example, the logClass function is a decorator. It takes the constructor of the class as an argument and logs a message when the class is defined.
Decorator Factories
A decorator factory is a function that returns a decorator. This allows you to pass arguments to the decorator.
function logClassWithMessage(message: string) {
return function(constructor: Function) {
console.log(`${message}: ${constructor.name}`);
};
}
@logClassWithMessage('Custom message for')
class AnotherClass {
constructor() {
console.log('Inside AnotherClass constructor');
}
}
Here, logClassWithMessage is a decorator factory. It takes a message parameter and returns a decorator function.
Decorator Composition
Multiple decorators can be applied to a single declaration. The order of execution of decorators is important. Decorators are evaluated from bottom to top, but applied from top to bottom.
function first() {
console.log('Evaluating first decorator');
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log('Applying first decorator');
};
}
function second() {
console.log('Evaluating second decorator');
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log('Applying second decorator');
};
}
class MyClassWithMultipleDecorators {
@first()
@second()
method() {}
}
Typical Usage Scenarios
Class Decorators
Class decorators are applied to the constructor of a class. They can be used to modify the constructor or add new functionality to the class.
function addStaticProperty(constructor: Function) {
constructor['staticProperty'] = 'This is a static property';
}
@addStaticProperty
class MyClassWithStaticProperty {
constructor() {}
}
console.log(MyClassWithStaticProperty['staticProperty']);
Method Decorators
Method decorators are applied to a method of a class. They can be used to modify the behavior of the method, such as adding logging or validation.
function logMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Calling method ${propertyKey} with arguments: ${JSON.stringify(args)}`);
const result = originalMethod.apply(this, args);
console.log(`Method ${propertyKey} returned: ${JSON.stringify(result)}`);
return result;
};
return descriptor;
}
class MyClassWithMethodDecorator {
@logMethod
add(a: number, b: number) {
return a + b;
}
}
const instanceWithMethodDecorator = new MyClassWithMethodDecorator();
instanceWithMethodDecorator.add(1, 2);
Property Decorators
Property decorators are applied to a property of a class. They can be used to add metadata or modify the behavior of property access.
function readOnly(target: any, propertyKey: string) {
let descriptor: PropertyDescriptor = {
writable: false
};
Object.defineProperty(target, propertyKey, descriptor);
}
class MyClassWithPropertyDecorator {
@readOnly
public myProperty = 'Initial value';
}
const instanceWithPropertyDecorator = new MyClassWithPropertyDecorator();
// This will throw an error because the property is read-only
// instanceWithPropertyDecorator.myProperty = 'New value';
Parameter Decorators
Parameter decorators are applied to a parameter of a method. They can be used to add metadata about the parameter.
function logParameter(target: any, propertyKey: string, parameterIndex: number) {
console.log(`Parameter at index ${parameterIndex} of method ${propertyKey} was logged.`);
}
class MyClassWithParameterDecorator {
method(@logParameter param: string) {}
}
Common Best Practices
Using Decorators for Logging
As shown in the method decorator example, decorators can be used to add logging to methods. This can be useful for debugging and monitoring the application.
Validation with Decorators
Decorators can be used to add validation logic to methods or properties. For example, a decorator can be used to validate that a parameter is a valid email address.
function validateEmail(target: any, propertyKey: string, parameterIndex: number) {
const originalMethod = target[propertyKey];
target[propertyKey] = function(...args: any[]) {
const email = args[parameterIndex];
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new Error('Invalid email address');
}
return originalMethod.apply(this, args);
};
}
class UserService {
createUser(@validateEmail email: string) {
console.log(`Creating user with email: ${email}`);
}
}
const userService = new UserService();
// This will throw an error
// userService.createUser('invalidemail');
userService.createUser('[email protected]');
Dependency Injection with Decorators
Decorators can be used to implement dependency injection. For example, a decorator can be used to mark a property as a dependency that needs to be injected.
function Injectable(target: any) {
target['isInjectable'] = true;
}
@Injectable
class DatabaseService {
constructor() {}
}
function Inject(target: any, propertyKey: string) {
const dependency = new (target.constructor[propertyKey])();
target[propertyKey] = dependency;
}
class UserRepository {
@Inject
databaseService: DatabaseService;
constructor() {}
getData() {
console.log('Getting data from database service');
}
}
const userRepository = new UserRepository();
userRepository.getData();
Conclusion
TypeScript decorators are a powerful feature that allows developers to add metadata and behavior to classes, methods, properties, and parameters. They provide a way to write more modular and reusable code. By understanding the core concepts, typical usage scenarios, and best practices, developers can leverage decorators to improve the quality and maintainability of their TypeScript applications.
FAQ
Q1: Are decorators a part of the ECMAScript standard?
A1: As of now, decorators are a stage 2 proposal in the ECMAScript standard. TypeScript has implemented them as an experimental feature.
Q2: Can I use decorators in JavaScript?
A2: Decorators are not natively supported in JavaScript. However, you can use TypeScript to write decorators and then transpile the code to JavaScript.
Q3: What is the difference between a decorator and a decorator factory?
A3: A decorator is a function that is directly applied to a declaration. A decorator factory is a function that returns a decorator, allowing you to pass arguments to the decorator.