TypeScript Decorators — Complete Guide
Advertisement
Decorators bring metaprogramming to TypeScript, enabling you to modify classes, methods, and properties at design time. They're powerful for frameworks like NestJS, TypeORM, and class-validator, and understanding them unlocks advanced patterns.
What Are Decorators?
Decorators are functions that augment classes, methods, properties, or parameters. They're prefixed with an @ symbol and execute when the decorated item is defined, not when it's used.
// Enable decorators in tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
// Simple decorator
function logged(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Calling ${propertyKey} with`, args);
return original.apply(this, args);
};
return descriptor;
}
class Calculator {
@logged
add(a: number, b: number): number {
return a + b;
}
}
const calc = new Calculator();
calc.add(2, 3); // Logs: Calling add with [2, 3]
Class Decorators
- What Are Decorators?
- Class Decorators
- Property Decorators
- Method Decorators
- Parameter Decorators
- Advanced Decorator Patterns
- Dependency Injection
- Type-Safe Decorators with Reflect
- Real-World Examples
- TypeORM-Style Decorators
- Route Decorators
- Best Practices
- Limitations and Alternatives
- FAQ
// Basic class decorator
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class User {
name: string = "";
}
// Decorator with arguments
function entity(tableName: string) {
return function<T extends { new(...args: any[]): {} }>(constructor: T) {
return class extends constructor {
static tableName = tableName;
};
};
}
@entity("users")
class UserEntity {
id: number = 0;
name: string = "";
}
console.log((UserEntity as any).tableName); // "users"
Property Decorators
// Property decorator
function Range(min: number, max: number) {
return function(target: Object, propertyKey: string | symbol) {
let value: any;
const getter = function() {
return value;
};
const setter = function(newVal: any) {
if (newVal < min || newVal > max) {
throw new Error(
`${String(propertyKey)} must be between ${min} and ${max}`
);
}
value = newVal;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
};
}
class Product {
@Range(0, 100)
discount: number = 0;
}
const product = new Product();
product.discount = 50; // OK
product.discount = 150; // Throws error
Method Decorators
// Timing decorator
function timed(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = async function(...args: any[]) {
const start = performance.now();
const result = await original.apply(this, args);
const end = performance.now();
console.log(`${propertyKey} took ${end - start}ms`);
return result;
};
return descriptor;
}
class DataService {
@timed
async fetchUsers(): Promise<any[]> {
return new Promise((resolve) => {
setTimeout(() => resolve([]), 1000);
});
}
}
// Authorization decorator
function authorize(roles: string[]) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
const user = (this as any).currentUser;
if (!user || !roles.includes(user.role)) {
throw new Error("Unauthorized");
}
return original.apply(this, args);
};
return descriptor;
};
}
class AdminService {
currentUser = { role: "admin" };
@authorize(["admin"])
deleteUser(userId: number): void {
console.log(`Deleting user ${userId}`);
}
}
Parameter Decorators
// Parameter decorator for validation
function Validate(target: any, propertyKey: string | symbol, parameterIndex: number) {
const existingMetadata = Reflect.getOwnMetadata("validate", target, propertyKey) || [];
existingMetadata.push(parameterIndex);
Reflect.defineMetadata("validate", existingMetadata, target, propertyKey);
}
function ValidateEmail(target: any, propertyKey: string | symbol, parameterIndex: number) {
const existingMetadata = Reflect.getOwnMetadata("validateEmail", target, propertyKey) || [];
existingMetadata.push(parameterIndex);
Reflect.defineMetadata("validateEmail", existingMetadata, target, propertyKey);
}
class UserController {
createUser(@ValidateEmail email: string): void {
console.log(`Creating user with email: ${email}`);
}
}
Advanced Decorator Patterns
Dependency Injection
// Simplified DI decorator
class Container {
private services = new Map<string, any>();
register<T>(key: string, factory: () => T): void {
this.services.set(key, factory());
}
get<T>(key: string): T {
return this.services.get(key);
}
}
const container = new Container();
function Injectable(target: Function) {
const instance = new (target as any)();
container.register(target.name, () => instance);
}
interface Logger {
log(message: string): void;
}
@Injectable
class ConsoleLogger implements Logger {
log(message: string): void {
console.log(message);
}
}
function Inject(serviceKey: string) {
return function(target: Object, propertyKey: string | symbol) {
Object.defineProperty(target, propertyKey, {
get: () => container.get(serviceKey),
});
};
}
class UserService {
@Inject("ConsoleLogger")
logger!: Logger;
getUser(): void {
this.logger.log("Getting user");
}
}
Type-Safe Decorators with Reflect
import "reflect-metadata";
// Reflect metadata for type info
function Serializable(target: Function) {
const types = Reflect.getOwnMetadata("design:paramtypes", target);
console.log("Constructor parameters:", types);
}
// Property type metadata
function Column(target: Object, propertyKey: string) {
const type = Reflect.getMetadata("design:type", target, propertyKey);
console.log(`${String(propertyKey)} is type ${type.name}`);
}
@Serializable
class User {
@Column
id: number = 0;
@Column
name: string = "";
}
Real-World Examples
TypeORM-Style Decorators
function Entity(tableName: string) {
return function<T extends { new(...args: any[]): {} }>(constructor: T) {
(constructor as any).tableName = tableName;
return constructor;
};
}
function Column(type?: "string" | "number" | "date") {
return function(target: Object, propertyKey: string | symbol) {
if (!Reflect.hasMetadata("columns", target.constructor)) {
Reflect.defineMetadata("columns", [], target.constructor);
}
const columns = Reflect.getMetadata("columns", target.constructor);
columns.push({ name: propertyKey, type });
};
}
@Entity("users")
class User {
@Column("number")
id: number = 0;
@Column("string")
name: string = "";
@Column("date")
createdAt: Date = new Date();
}
Route Decorators
function Get(path: string) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.value.method = "GET";
descriptor.value.path = path;
return descriptor;
};
}
function Post(path: string) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.value.method = "POST";
descriptor.value.path = path;
return descriptor;
};
}
class UserController {
@Get("/users")
listUsers(): void {}
@Post("/users")
createUser(): void {}
}
Best Practices
- Use decorators sparingly - they can make code harder to understand
- Document decorator behavior - side effects aren't obvious
- Combine with types - use generics for type-safe decorators
- Test decorated classes thoroughly - decorators modify behavior
Limitations and Alternatives
- Decorators are experimental - API may change
- Performance overhead - decorators add runtime cost
- Complexity - can make debugging harder
- Consider alternatives - composition or middleware might be clearer
FAQ
Q: Are decorators production-ready? A: Yes, with experimentalDecorators enabled. Major frameworks use them. Stage 3 proposal means standardization is in progress.
Q: Can I stack multiple decorators? A: Yes, they execute bottom-to-top. @decorator1 @decorator2 calls decorator2 first, then decorator1.
Q: What's the performance impact? A: Minimal for most use cases. Decorators execute once at class definition, not per method call. Profile your specific use case if performance is critical.
Decorators transform TypeScript from a language with types into a metaprogramming powerhouse. Whether building frameworks or enterprise applications, mastering decorators opens advanced architectural patterns. Use them judiciously for maximum impact.
Advertisement