Decorators and Metadata in TypeScript
Decorators enable powerful metaprogramming capabilities. This article covers decorator types, metadata, and practical applications.
Introduction
Decorators provide:
- Code enhancement
- Metaprogramming
- Reflection
- Cross-cutting concerns
- Framework capabilities
Understanding decorators helps you:
- Enhance classes and methods
- Implement frameworks
- Add metadata
- Create reusable patterns
- Build powerful abstractions
Enabling Decorators
Configuration
// โ
Good: Enable decorators in tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"target": "ES2020",
"module": "commonjs"
}
}
Class Decorators
Basic Class Decorators
// โ
Good: Basic class decorator
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class User {
name: string;
constructor(name: string) {
this.name = name;
}
}
// โ
Good: Class decorator with parameters
function logged(target: Function) {
const original = target;
const newConstructor: any = function (...args: any[]) {
console.log(`Creating instance of ${original.name}`);
return new original(...args);
};
newConstructor.prototype = original.prototype;
return newConstructor;
}
@logged
class Product {
constructor(public name: string) {}
}
// โ
Good: Decorator factory
function decorator(param: string) {
return function (target: Function) {
console.log(`Decorating ${target.name} with ${param}`);
};
}
@decorator('param')
class MyClass {}
Practical Class Decorators
// โ
Good: Validation decorator
function validate(target: Function) {
const original = target;
const newConstructor: any = function (...args: any[]) {
const instance = new original(...args);
// Validate instance
for (const key in instance) {
if (typeof instance[key] === 'string' && instance[key].length === 0) {
throw new Error(`${key} cannot be empty`);
}
}
return instance;
};
newConstructor.prototype = original.prototype;
return newConstructor;
}
@validate
class User {
constructor(public name: string, public email: string) {}
}
// โ
Good: Singleton decorator
function singleton(target: Function) {
let instance: any;
const newConstructor: any = function (...args: any[]) {
if (!instance) {
instance = new target(...args);
}
return instance;
};
newConstructor.prototype = target.prototype;
return newConstructor;
}
@singleton
class Database {
constructor(public url: string) {}
}
Method Decorators
Basic Method Decorators
// โ
Good: Basic method decorator
function log(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with:`, args);
const result = originalMethod.apply(this, args);
console.log(`Result:`, result);
return result;
};
return descriptor;
}
class Calculator {
@log
add(a: number, b: number): number {
return a + b;
}
}
// โ
Good: Timing decorator
function timing(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const start = performance.now();
const result = originalMethod.apply(this, args);
const end = performance.now();
console.log(`${propertyKey} took ${end - start}ms`);
return result;
};
return descriptor;
}
class Service {
@timing
slowOperation() {
// Simulate slow operation
for (let i = 0; i < 1000000; i++) {}
}
}
Async Method Decorators
// โ
Good: Async method decorator
function asyncLog(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
console.log(`Calling async ${propertyKey}`);
try {
const result = await originalMethod.apply(this, args);
console.log(`${propertyKey} completed`);
return result;
} catch (error) {
console.error(`${propertyKey} failed:`, error);
throw error;
}
};
return descriptor;
}
class ApiService {
@asyncLog
async fetchUser(id: number) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
}
Property Decorators
Basic Property Decorators
// โ
Good: Property decorator
function required(target: any, propertyKey: string) {
let value: any;
const getter = function () {
return value;
};
const setter = function (newValue: any) {
if (!newValue) {
throw new Error(`${propertyKey} is required`);
}
value = newValue;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
class User {
@required
name: string;
@required
email: string;
}
// โ
Good: Validation property decorator
function validate(pattern: RegExp) {
return function (target: any, propertyKey: string) {
let value: any;
const getter = function () {
return value;
};
const setter = function (newValue: any) {
if (!pattern.test(newValue)) {
throw new Error(`${propertyKey} is invalid`);
}
value = newValue;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
};
}
class User {
@validate(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
email: string;
}
Metadata
Using Reflect Metadata
// โ
Good: Use reflect-metadata
// npm install reflect-metadata
import 'reflect-metadata';
function metadata(key: string, value: any) {
return function (target: any, propertyKey?: string) {
if (propertyKey) {
Reflect.defineMetadata(key, value, target, propertyKey);
} else {
Reflect.defineMetadata(key, value, target);
}
};
}
@metadata('version', '1.0.0')
class User {
@metadata('type', 'string')
name: string;
@metadata('type', 'number')
age: number;
}
// Retrieve metadata
const version = Reflect.getMetadata('version', User);
const nameType = Reflect.getMetadata('type', User.prototype, 'name');
Parameter Decorators
// โ
Good: Parameter decorator
function validate(
target: any,
propertyKey: string | symbol | undefined,
parameterIndex: number
) {
const existingMetadata = Reflect.getOwnMetadata('validate', target, propertyKey) || [];
existingMetadata.push(parameterIndex);
Reflect.defineMetadata('validate', existingMetadata, target, propertyKey);
}
class UserService {
createUser(@validate name: string, @validate email: string) {
// Implementation
}
}
Practical Decorators
Validation Decorator
// โ
Good: Comprehensive validation decorator
function validateInput(rules: Record<string, any>) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
for (const [key, rule] of Object.entries(rules)) {
const value = args[0]?.[key];
if (rule.required && !value) {
throw new Error(`${key} is required`);
}
if (rule.type && typeof value !== rule.type) {
throw new Error(`${key} must be ${rule.type}`);
}
if (rule.pattern && !rule.pattern.test(value)) {
throw new Error(`${key} is invalid`);
}
}
return originalMethod.apply(this, args);
};
return descriptor;
};
}
class UserService {
@validateInput({
name: { required: true, type: 'string' },
email: { required: true, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ }
})
createUser(data: { name: string; email: string }) {
// Implementation
}
}
Caching Decorator
// โ
Good: Caching decorator
function cache(ttl: number = 60000) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
const cache = new Map<string, { value: any; timestamp: number }>();
descriptor.value = function (...args: any[]) {
const key = JSON.stringify(args);
const cached = cache.get(key);
if (cached && Date.now() - cached.timestamp < ttl) {
console.log(`Cache hit for ${propertyKey}`);
return cached.value;
}
const result = originalMethod.apply(this, args);
cache.set(key, { value: result, timestamp: Date.now() });
return result;
};
return descriptor;
};
}
class DataService {
@cache(5000)
fetchData(id: number) {
console.log(`Fetching data for ${id}`);
return { id, data: 'some data' };
}
}
Best Practices
-
Use decorators for cross-cutting concerns:
// โ Good @logged @timed class Service {} // โ Bad class Service { log() { } time() { } } -
Enable experimental decorators:
// โ Good "experimentalDecorators": true // โ Bad // Decorators not enabled -
Use metadata for reflection:
// โ Good @metadata('version', '1.0') class MyClass {} // โ Bad class MyClass { version = '1.0'; }
Common Mistakes
-
Not enabling experimental decorators:
// โ Bad // tsconfig.json doesn't have experimentalDecorators // โ Good "experimentalDecorators": true -
Modifying prototype incorrectly:
// โ Bad function decorator(target: Function) { target.prototype.newMethod = () => {}; } // โ Good function decorator(target: Function) { const original = target; const newConstructor: any = function (...args: any[]) { return new original(...args); }; newConstructor.prototype = original.prototype; return newConstructor; }
Summary
Decorators are powerful. Key takeaways:
- Enable experimental decorators
- Use class decorators for enhancement
- Use method decorators for wrapping
- Use property decorators for validation
- Use metadata for reflection
- Apply to cross-cutting concerns
- Maintain clean code
- Document decorator behavior
Related Resources
Next Steps
- Learn about Advanced Type Patterns
- Explore TypeScript Best Practices
- Study Generics
- Practice decorator patterns
- Build framework features
Comments