TypeScript decorators provide powerful metaprogramming capabilities for modern web applications. Let's explore how to create and use decorators effectively for better code organization and reusability.
Understanding Decorators 🎯
Decorators enable:
- Metaprogramming capabilities
- Code reusability
- Aspect-oriented programming
- Runtime modifications
- Clean architecture patterns
Basic Decorator Implementation
Create class decorators:
function Logger(prefix: string) {
return function (target: any) {
// Store original methods
const original = target.prototype;
// Add logging to all methods
for (const key of Object.getOwnPropertyNames(original)) {
if (key !== 'constructor') {
const descriptor = Object.getOwnPropertyDescriptor(
original,
key
);
if (descriptor && typeof descriptor.value === 'function') {
Object.defineProperty(original, key, {
value: function (...args: any[]) {
console.log(
`${prefix} Calling ${key} with:`,
args
);
return descriptor.value.apply(this, args);
}
});
}
}
}
};
}
@Logger('MyClass')
class MyClass {
doSomething(value: string) {
return `Processed: ${value}`;
}
}
Method Decorators
Implement method-level decorators:
function Measure() {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
const start = performance.now();
try {
const result = await originalMethod.apply(this, args);
return result;
} finally {
const end = performance.now();
console.log(
`${propertyKey} took ${end - start}ms`
);
}
};
return descriptor;
};
}
class ApiService {
@Measure()
async fetchData(id: string) {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
return { id, data: 'Some data' };
}
}
Property Decorators
Create property decorators:
function ValidateLength(min: number, max: number) {
return function (target: any, propertyKey: string) {
let value: string;
const getter = function () {
return value;
};
const setter = function (newVal: string) {
if (newVal.length < min || newVal.length > max) {
throw new Error(
`${propertyKey} must be between ` +
`${min} and ${max} characters`
);
}
value = newVal;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter
});
};
}
class User {
@ValidateLength(3, 50)
name: string;
constructor(name: string) {
this.name = name;
}
}
Parameter Decorators
Implement parameter validation:
function Required() {
return function (
target: any,
propertyKey: string,
parameterIndex: number
) {
const requiredParams = Reflect.getMetadata(
'required',
target,
propertyKey
) || [];
requiredParams.push(parameterIndex);
Reflect.defineMetadata(
'required',
requiredParams,
target,
propertyKey
);
};
}
class UserService {
createUser(@Required() name: string, age?: number) {
return { name, age };
}
}
Advanced Patterns
Dependency Injection
Implement DI decorators:
const container = new Map();
function Injectable() {
return function (target: any) {
container.set(target.name, new target());
};
}
function Inject(token: string) {
return function (
target: any,
propertyKey: string
) {
Object.defineProperty(target, propertyKey, {
get: () => container.get(token)
});
};
}
@Injectable()
class UserRepository {
findById(id: string) {
return { id, name: 'John' };
}
}
@Injectable()
class UserController {
@Inject('UserRepository')
private userRepository: UserRepository;
getUser(id: string) {
return this.userRepository.findById(id);
}
}
Caching Decorator
Implement method caching:
function Cacheable(ttl: number = 60000) {
const cache = new Map<string, {
value: any;
timestamp: number;
}>();
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
const key = `${propertyKey}-${JSON.stringify(args)}`;
const cached = cache.get(key);
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.value;
}
const result = await originalMethod.apply(this, args);
cache.set(key, {
value: result,
timestamp: Date.now()
});
return result;
};
return descriptor;
};
}
Validation Decorators
Create validation decorators:
function Validate(schema: any) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const validation = schema.validate(args[0]);
if (validation.error) {
throw new Error(validation.error.message);
}
return originalMethod.apply(this, args);
};
return descriptor;
};
}
const userSchema = {
validate: (data: any) => {
if (!data.name || data.name.length < 3) {
return {
error: { message: 'Invalid name' }
};
}
return { error: null };
}
};
class UserService {
@Validate(userSchema)
createUser(userData: any) {
return userData;
}
}
Best Practices
- Decorator Design
- Keep decorators focused
- Use meaningful names
- Document behavior
- Handle errors properly
- Performance
- Minimize runtime overhead
- Cache computed values
- Optimize metadata usage
- Monitor impact
- Maintainability
- Follow single responsibility
- Use composition
- Document dependencies
- Test thoroughly
- Development
- Use TypeScript strict mode
- Implement proper typing
- Handle edge cases
- Monitor decorator usage
Testing Decorators
Implement decorator tests:
describe('Measure Decorator', () => {
it('should measure execution time', async () => {
const spy = jest.spyOn(console, 'log');
class TestClass {
@Measure()
async testMethod() {
await new Promise(
resolve => setTimeout(resolve, 100)
);
}
}
const instance = new TestClass();
await instance.testMethod();
expect(spy).toHaveBeenCalled();
expect(spy.mock.calls[0][0]).toMatch(
/testMethod took \d+ms/
);
});
});
Conclusion
TypeScript decorators provide powerful tools for metaprogramming and code organization. Remember to:
- Design focused decorators
- Consider performance impact
- Test thoroughly
- Document behavior
- Handle errors properly
- Follow best practices
As decorators become more widely used, these patterns will become essential for modern TypeScript development.