TypeScript Decorators
A decorator is a special function that attaches extra behavior to a class, method, property, or parameter without modifying the original code. Decorators use the @ symbol followed by the decorator name. They run at the time a class is defined — not when objects are created — and work like wrappers that observe or modify the target they decorate.
Enabling Decorators
Decorators require the experimentalDecorators flag enabled in tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
How Decorators Work
Without decorator: With decorator:
+------------------+ +------------------+
| class Product { | @Log | class Product { |
| getName() { } | ---------> | getName() { } |
| } | wraps it | + logging |
+------------------+ | + timing |
+------------------+
Original class unchanged Class gains new behavior
Class Decorators
A class decorator receives the constructor function of the class. It can observe, modify, or replace the class.
// Simple logging decorator
function LogClass(constructor: Function): void {
console.log("Class created: " + constructor.name);
}
@LogClass
class UserService {
createUser(name: string): void {
console.log("Creating user: " + name);
}
}
// Output on class definition: Class created: UserService
let service = new UserService();
service.createUser("Rekha"); // Creating user: Rekha
Class Decorator with Modification
// Decorator that adds a 'version' property to any class
function Versioned(version: string) {
return function(constructor: Function): void {
constructor.prototype.version = version;
};
}
@Versioned("2.0.0")
class AppConfig {
appName: string = "eStudy247";
}
let config = new AppConfig() as AppConfig & { version: string };
console.log(config.appName); // eStudy247
console.log(config.version); // 2.0.0
Method Decorators
A method decorator wraps a class method. It receives the target object, the method name, and the property descriptor. This is the most commonly used type of decorator.
// Decorator that logs method calls and execution time
function LogMethod(
target: any,
methodName: string,
descriptor: PropertyDescriptor
): PropertyDescriptor {
let originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log("Calling: " + methodName + "(" + args.join(", ") + ")");
let start = Date.now();
let result = originalMethod.apply(this, args);
let elapsed = Date.now() - start;
console.log(methodName + " completed in " + elapsed + "ms");
return result;
};
return descriptor;
}
class Calculator {
@LogMethod
add(a: number, b: number): number {
return a + b;
}
}
let calc = new Calculator();
let result = calc.add(10, 20);
// Calling: add(10, 20)
// add completed in 0ms
console.log(result); // 30
Property Decorators
A property decorator receives the target object and the property name. It can observe or transform property behavior.
// Decorator that marks a property as readonly at runtime
function ReadOnly(target: any, propertyKey: string): void {
Object.defineProperty(target, propertyKey, {
writable: false,
enumerable: true,
configurable: false
});
}
class Config {
@ReadOnly
appName: string = "eStudy247";
version: string = "1.0";
}
let cfg = new Config();
console.log(cfg.appName); // eStudy247
cfg.appName = "Other"; // Fails silently (or throws in strict mode)
cfg.version = "2.0"; // Works normally
Parameter Decorators
// Decorator that logs which parameter index was decorated
function LogParam(target: any, methodName: string, paramIndex: number): void {
console.log("Decorated parameter " + paramIndex + " in method: " + methodName);
}
class NotificationService {
send(@LogParam message: string, recipient: string): void {
console.log("Sending '" + message + "' to " + recipient);
}
}
// Output when class loads: Decorated parameter 0 in method: send
Accessor Decorators
An accessor decorator applies to get or set accessors of a class property. It modifies how getting or setting the value works.
function Validate(target: any, name: string, descriptor: PropertyDescriptor): PropertyDescriptor {
let originalSet = descriptor.set!;
descriptor.set = function(value: number) {
if (value < 0) {
throw new Error(name + " cannot be negative.");
}
originalSet.call(this, value);
};
return descriptor;
}
class Product {
private _price: number = 0;
@Validate
set price(value: number) {
this._price = value;
}
get price(): number {
return this._price;
}
}
let p = new Product();
p.price = 500;
console.log(p.price); // 500
p.price = -10; // Error: price cannot be negative.
Decorator Factories
A decorator factory is a function that returns a decorator. This allows decorators to accept custom arguments.
// Factory that accepts a role name
function RequireRole(role: string) {
return function(target: any, methodName: string, descriptor: PropertyDescriptor): PropertyDescriptor {
let original = descriptor.value;
descriptor.value = function(this: any, ...args: any[]) {
if (this.userRole !== role) {
throw new Error("Access denied. Requires role: " + role);
}
return original.apply(this, args);
};
return descriptor;
};
}
class AdminPanel {
userRole: string = "admin";
@RequireRole("admin")
deleteUser(userId: number): void {
console.log("User " + userId + " deleted.");
}
}
let panel = new AdminPanel();
panel.deleteUser(5); // User 5 deleted.
panel.userRole = "viewer";
panel.deleteUser(5); // Error: Access denied. Requires role: admin
Decorator Execution Order
Multiple decorators on one target — execute bottom to top:
@First ← executes second
@Second ← executes first
class MyClass { }
Method decorator evaluation:
1. Parameter decorators (innermost first)
2. Method decorators
3. Accessor decorators
4. Property decorators
5. Class decorators (last)
Decorator Types Summary
| Decorator Type | Placement | Receives | Common Use |
|---|---|---|---|
| Class | Above class | Constructor | Logging, versioning, metadata |
| Method | Above method | Target, name, descriptor | Logging, caching, auth checks |
| Property | Above property | Target, name | Validation, readonly enforcement |
| Accessor | Above get/set | Target, name, descriptor | Validation on set, formatting on get |
| Parameter | Before parameter | Target, method name, index | Dependency injection, logging |
Practical Example
// Memoization decorator — caches function results
function Memoize(target: any, name: string, descriptor: PropertyDescriptor): PropertyDescriptor {
let cache = new Map<string, any>();
let original = descriptor.value;
descriptor.value = function(...args: any[]) {
let key = JSON.stringify(args);
if (cache.has(key)) {
console.log("Cache hit for: " + name + "(" + key + ")");
return cache.get(key);
}
let result = original.apply(this, args);
cache.set(key, result);
return result;
};
return descriptor;
}
class MathService {
@Memoize
fibonacci(n: number): number {
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}
let ms = new MathService();
console.log(ms.fibonacci(10)); // 55
console.log(ms.fibonacci(10)); // Cache hit — 55 (instant)
console.log(ms.fibonacci(8)); // 21
