What is decorators in TypeScript?
Decorators are a special kind of declaration that can be attached to classes, methods, accessors, properties, or parameters. They are functions that allow you to add annotations and a meta-programming syntax for class declarations and members.
What are Decorators?
Decorators are an experimental feature in TypeScript that provides a way to add annotations and a meta-programming syntax for class declarations and members. They are functions that are executed at declaration time, allowing you to observe, modify, or replace class definitions and their members.
Essentially, decorators provide a way to add metadata or alter the behavior of classes and their members without modifying their underlying code directly. This makes them powerful tools for architectural patterns like Aspect-Oriented Programming (AOP), dependency injection, and framework integration.
Enabling Decorators
Before using decorators, you must enable the experimentalDecorators compiler option in your tsconfig.json file. You might also need emitDecoratorMetadata if you are working with libraries that require reflection metadata (e.g., Angular, TypeORM).
{
"compilerOptions": {
"target": "ES2016",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Types of Decorators
- Class Decorators: Applied to a class constructor. Can be used to observe, modify, or replace a class definition.
- Method Decorators: Applied to a method declaration. Can observe, modify, or replace a method definition.
- Accessor Decorators: Applied to an accessor (get/set) declaration. Can observe, modify, or replace an accessor's definition.
- Property Decorators: Applied to a property declaration. Can observe or modify a property's definition.
- Parameter Decorators: Applied to a parameter declaration within a method or constructor. Can observe or modify a parameter's definition.
How Decorators Work
Decorators are simply functions. When a decorator is applied, TypeScript calls this function with specific arguments depending on the type of declaration it's decorating. For instance, a class decorator receives the constructor function of the class, while a method decorator receives the target object, the property name (method name), and the property descriptor.
They execute at declaration time, which is when the JavaScript is compiled, not at runtime when an instance of the class is created. This allows them to effectively 'wrap' or 'enhance' the declarations they target.
Decorator Factories
When you need to pass arguments to a decorator or set up some configuration, you can use a decorator factory. A decorator factory is a function that returns the actual decorator function. This allows for greater flexibility and dynamic behavior.
function Logger(logString: string) {
return function(constructor: Function) {
console.log(logString);
console.log(constructor);
};
}
@Logger('LOGGING - PERSON')
class Person {
name = 'Max';
constructor() {
console.log('Creating person object...');
}
}
const pers = new Person(); // Output in console during compilation/setup: LOGGING - PERSON, [Function: Person]. Output at runtime: Creating person object...
Example: Method Decorator
Let's illustrate with a simple method decorator that logs when a method is called and can prevent it from running based on a condition.
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} finished. Result: ${result}`);
return result;
};
return descriptor;
}
class Calculator {
@LogMethod
add(a: number, b: number) {
return a + b;
}
@LogMethod
subtract(a: number, b: number) {
return a - b;
}
}
const calc = new Calculator();
calc.add(5, 3);
calc.subtract(10, 4);
/*
Output:
Calling method: add with arguments: [5,3]
Method add finished. Result: 8
Calling method: subtract with arguments: [10,4]
Method subtract finished. Result: 6
*/
Common Use Cases
- Aspect-Oriented Programming (AOP): Adding cross-cutting concerns (e.g., logging, caching, authorization) to multiple parts of an application without repeating code.
- Dependency Injection: Used in frameworks like Angular for managing dependencies and service instantiation.
- Serialization/Deserialization: Mapping objects to and from JSON or other formats (e.g.,
class-transformer). - ORM/Database Mapping: Defining how properties map to database columns (e.g., TypeORM, MikroORM).
- Validation: Attaching validation rules to properties or parameters (e.g.,
class-validator). - Framework Integration: Providing declarative syntax for configuring components, routes, or services in frameworks.
Summary
Decorators in TypeScript offer a powerful way to extend and modify the behavior of classes and their members in a declarative and less intrusive manner. While still an experimental feature, they are widely adopted in modern TypeScript frameworks and libraries, enabling cleaner, more maintainable, and highly configurable codebases.