Master TypeScript decorators for metaprogramming, aspect-oriented programming, and runtime behavior modification. Learn to build powerful frameworks and libraries using decorator patterns.
Decorators are TypeScript's most powerful metaprogramming feature, enabling you to modify classes, methods, and properties at design time. They're the foundation of modern frameworks like Angular, NestJS, and TypeORM, providing clean APIs for cross-cutting concerns.
While decorators are still experimental in TypeScript, they're widely used in production and will become a standard feature. Understanding decorators opens up advanced architectural patterns including dependency injection, aspect-oriented programming, and declarative configuration.
In this lesson, you'll learn to create sophisticated decorator systems that can transform how your applications are structured and how your APIs are designed.
Decorators provide a declarative way to add metadata and modify the behavior of classes and their members at design time.
Basic Decorator Concepts:
1// Enable experimental decorators in tsconfig.json
2{
3 "compilerOptions": {
4 "experimentalDecorators": true,
5 "emitDecoratorMetadata": true
6 }
7}
8
9// Simple decorator function
10function logged(target: any, propertyName: string, descriptor: PropertyDescriptor) {
11 const method = descriptor.value;
12
13 descriptor.value = function (...args: any[]) {
14 console.log(`Calling ${propertyName} with arguments: ${JSON.stringify(args)}`);
15 const result = method.apply(this, args);
16 console.log(`${propertyName} returned: ${JSON.stringify(result)}`);
17 return result;
18 };
19}
20
21class Calculator {
22 @logged
23 add(a: number, b: number): number {
24 return a + b;
25 }
26
27 @logged
28 multiply(a: number, b: number): number {
29 return a * b;
30 }
31}
32
33const calc = new Calculator();
34calc.add(5, 3); // Logs: Calling add with arguments: [5,3]
35 // add returned: 8
36calc.multiply(4, 6); // Logs: Calling multiply with arguments: [4,6]
37 // multiply returned: 24Decorator Factory Pattern:
1// Decorator factory allows configuration
2function cache(ttl: number = 60000) {
3 const cache = new Map<string, { value: any; expires: number }>();
4
5 return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
6 const originalMethod = descriptor.value;
7
8 descriptor.value = function (...args: any[]) {
9 const key = `${propertyName}-${JSON.stringify(args)}`;
10 const cached = cache.get(key);
11
12 if (cached && Date.now() < cached.expires) {
13 console.log(`Cache hit for ${propertyName}`);
14 return cached.value;
15 }
16
17 const result = originalMethod.apply(this, args);
18 cache.set(key, {
19 value: result,
20 expires: Date.now() + ttl
21 });
22
23 console.log(`Cache miss for ${propertyName}, result cached`);
24 return result;
25 };
26 };
27}
28
29class ApiService {
30 @cache(30000) // 30 second TTL
31 async fetchUser(id: string): Promise<{ id: string; name: string }> {
32 // Simulate API call
33 await new Promise(resolve => setTimeout(resolve, 1000));
34 return { id, name: `User ${id}` };
35 }
36
37 @cache(60000) // 1 minute TTL
38 async fetchPosts(): Promise<string[]> {
39 await new Promise(resolve => setTimeout(resolve, 500));
40 return ['Post 1', 'Post 2', 'Post 3'];
41 }
42}Multiple Decorators and Execution Order:
1function first() {
2 console.log("first(): factory evaluated");
3 return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
4 console.log("first(): called");
5 };
6}
7
8function second() {
9 console.log("second(): factory evaluated");
10 return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
11 console.log("second(): called");
12 };
13}
14
15class ExampleClass {
16 @first()
17 @second()
18 method() {}
19}
20
21// Output:
22// first(): factory evaluated
23// second(): factory evaluated
24// second(): called
25// first(): called
26
27// Decorators are applied bottom-to-top (composition order)Conditional Decorators:
1function conditionalLog(condition: () => boolean) {
2 return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
3 const originalMethod = descriptor.value;
4
5 descriptor.value = function (...args: any[]) {
6 if (condition()) {
7 console.log(`[${new Date().toISOString()}] Executing ${propertyName}`);
8 }
9
10 return originalMethod.apply(this, args);
11 };
12 };
13}
14
15// Only log in development
16const isDevelopment = () => process.env.NODE_ENV === 'development';
17
18class ProductService {
19 @conditionalLog(isDevelopment)
20 createProduct(data: any) {
21 return { id: Date.now(), ...data };
22 }
23
24 @conditionalLog(() => true) // Always log this critical operation
25 deleteProduct(id: string) {
26 console.log(`Deleting product ${id}`);
27 }
28}TypeScript supports four types of decorators, each with specific signatures and use cases for different aspects of metaprogramming.
Class Decorators:
1// Class decorator signature: (constructor: Function) => Function | void
2function injectable<T extends { new (...args: any[]): {} }>(constructor: T) {
3 // Add dependency injection metadata
4 (constructor as any).__injectable = true;
5 (constructor as any).__dependencies = Reflect.getMetadata('design:paramtypes', constructor) || [];
6
7 return class extends constructor {
8 __injected = true;
9
10 constructor(...args: any[]) {
11 super(...args);
12 console.log(`Creating instance of ${constructor.name}`);
13 }
14 };
15}
16
17@injectable
18class UserService {
19 constructor(private apiClient: any, private logger: any) {}
20
21 getUser(id: string) {
22 return { id, name: `User ${id}` };
23 }
24}
25
26// Sealed class decorator prevents extension
27function sealed(constructor: Function) {
28 Object.seal(constructor);
29 Object.seal(constructor.prototype);
30}
31
32@sealed
33class ImmutableClass {
34 value: string = "cannot be extended";
35}
36
37// Component registration decorator
38const components = new Map<string, Function>();
39
40function component(name: string) {
41 return function <T extends { new (...args: any[]): {} }>(constructor: T) {
42 components.set(name, constructor);
43
44 // Add component metadata
45 (constructor as any).__componentName = name;
46 (constructor as any).__isComponent = true;
47
48 return constructor;
49 };
50}
51
52@component('user-profile')
53class UserProfileComponent {
54 render() {
55 return '<div>User Profile</div>';
56 }
57}
58
59@component('navigation')
60class NavigationComponent {
61 render() {
62 return '<nav>Navigation</nav>';
63 }
64}
65
66// Factory function to create components
67function createComponent(name: string, ...args: any[]) {
68 const ComponentClass = components.get(name);
69 if (!ComponentClass) {
70 throw new Error(`Component '${name}' not found`);
71 }
72 return new ComponentClass(...args);
73}Method Decorators:
1// Method decorator signature: (target: any, propertyName: string, descriptor: PropertyDescriptor) => PropertyDescriptor | void
2
3function retry(maxAttempts: number = 3, delay: number = 1000) {
4 return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
5 const originalMethod = descriptor.value;
6
7 descriptor.value = async function (...args: any[]) {
8 let lastError: Error;
9
10 for (let attempt = 1; attempt <= maxAttempts; attempt++) {
11 try {
12 return await originalMethod.apply(this, args);
13 } catch (error) {
14 lastError = error as Error;
15
16 if (attempt < maxAttempts) {
17 console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
18 await new Promise(resolve => setTimeout(resolve, delay));
19 }
20 }
21 }
22
23 throw lastError!;
24 };
25 };
26}
27
28function timeout(ms: number) {
29 return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
30 const originalMethod = descriptor.value;
31
32 descriptor.value = async function (...args: any[]) {
33 return Promise.race([
34 originalMethod.apply(this, args),
35 new Promise((_, reject) =>
36 setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
37 )
38 ]);
39 };
40 };
41}
42
43function benchmark(target: any, propertyName: string, descriptor: PropertyDescriptor) {
44 const originalMethod = descriptor.value;
45
46 descriptor.value = function (...args: any[]) {
47 const start = performance.now();
48 const result = originalMethod.apply(this, args);
49 const end = performance.now();
50
51 console.log(`${propertyName} took ${(end - start).toFixed(2)}ms`);
52 return result;
53 };
54}
55
56class NetworkService {
57 @retry(3, 2000)
58 @timeout(10000)
59 async fetchData(url: string): Promise<any> {
60 const response = await fetch(url);
61 if (!response.ok) {
62 throw new Error(`HTTP ${response.status}`);
63 }
64 return response.json();
65 }
66
67 @benchmark
68 processLargeDataset(data: any[]): any[] {
69 // Simulate expensive operation
70 return data.map(item => ({ ...item, processed: true }));
71 }
72}Property Decorators:
1// Property decorator signature: (target: any, propertyName: string) => void
2
3function required(target: any, propertyName: string) {
4 // Store required properties metadata
5 const requiredProperties = Reflect.getMetadata('required', target) || [];
6 requiredProperties.push(propertyName);
7 Reflect.defineMetadata('required', requiredProperties, target);
8}
9
10function format(formatter: (value: any) => any) {
11 return function (target: any, propertyName: string) {
12 let value: any;
13
14 const getter = function () {
15 return value;
16 };
17
18 const setter = function (newValue: any) {
19 value = formatter(newValue);
20 };
21
22 Object.defineProperty(target, propertyName, {
23 get: getter,
24 set: setter,
25 enumerable: true,
26 configurable: true
27 });
28 };
29}
30
31function validate(validator: (value: any) => boolean, message: string) {
32 return function (target: any, propertyName: string) {
33 let value: any;
34
35 const getter = function () {
36 return value;
37 };
38
39 const setter = function (newValue: any) {
40 if (!validator(newValue)) {
41 throw new Error(`Validation failed for ${propertyName}: ${message}`);
42 }
43 value = newValue;
44 };
45
46 Object.defineProperty(target, propertyName, {
47 get: getter,
48 set: setter,
49 enumerable: true,
50 configurable: true
51 });
52 };
53}
54
55class UserModel {
56 @required
57 @validate((value) => typeof value === 'string' && value.length > 0, 'Name must be a non-empty string')
58 name: string;
59
60 @required
61 @validate((value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value), 'Must be a valid email')
62 email: string;
63
64 @format((value) => value.toString().toLowerCase())
65 username: string;
66
67 @validate((value) => value >= 0 && value <= 150, 'Age must be between 0 and 150')
68 age: number;
69
70 constructor(data: any) {
71 Object.assign(this, data);
72 this.validateRequired();
73 }
74
75 private validateRequired() {
76 const requiredProperties = Reflect.getMetadata('required', this) || [];
77 for (const prop of requiredProperties) {
78 if (this[prop] === undefined || this[prop] === null) {
79 throw new Error(`Required property '${prop}' is missing`);
80 }
81 }
82 }
83}Parameter Decorators:
1// Parameter decorator signature: (target: any, propertyName: string, parameterIndex: number) => void
2
3function inject(token: string) {
4 return function (target: any, propertyName: string | undefined, parameterIndex: number) {
5 const existingTokens = Reflect.getMetadata('inject-tokens', target) || [];
6 existingTokens[parameterIndex] = token;
7 Reflect.defineMetadata('inject-tokens', existingTokens, target);
8 };
9}
10
11function validate(validator: (value: any) => boolean) {
12 return function (target: any, propertyName: string, parameterIndex: number) {
13 const originalMethod = target[propertyName];
14
15 target[propertyName] = function (...args: any[]) {
16 if (!validator(args[parameterIndex])) {
17 throw new Error(`Invalid parameter at index ${parameterIndex}`);
18 }
19 return originalMethod.apply(this, args);
20 };
21 };
22}
23
24function transform(transformer: (value: any) => any) {
25 return function (target: any, propertyName: string, parameterIndex: number) {
26 const originalMethod = target[propertyName];
27
28 target[propertyName] = function (...args: any[]) {
29 args[parameterIndex] = transformer(args[parameterIndex]);
30 return originalMethod.apply(this, args);
31 };
32 };
33}
34
35class OrderService {
36 constructor(
37 @inject('payment-service') private paymentService: any,
38 @inject('inventory-service') private inventoryService: any,
39 @inject('logger') private logger: any
40 ) {}
41
42 createOrder(
43 @validate((id) => typeof id === 'string' && id.length > 0) userId: string,
44 @validate((items) => Array.isArray(items) && items.length > 0) items: any[],
45 @transform((amount) => Math.round(amount * 100) / 100) totalAmount: number
46 ) {
47 this.logger.info(`Creating order for user ${userId} with ${items.length} items`);
48 return {
49 id: Date.now().toString(),
50 userId,
51 items,
52 totalAmount,
53 status: 'pending'
54 };
55 }
56}
57
58// Dependency injection container
59class DIContainer {
60 private services = new Map<string, any>();
61
62 register(token: string, service: any) {
63 this.services.set(token, service);
64 }
65
66 resolve<T>(constructor: new (...args: any[]) => T): T {
67 const tokens = Reflect.getMetadata('inject-tokens', constructor) || [];
68 const dependencies = tokens.map((token: string) => {
69 const service = this.services.get(token);
70 if (!service) {
71 throw new Error(`Service '${token}' not found`);
72 }
73 return service;
74 });
75
76 return new constructor(...dependencies);
77 }
78}Advanced Decorator Composition:
1// Combine multiple decorator patterns
2function api(endpoint: string) {
3 return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
4 // Store API metadata
5 Reflect.defineMetadata('api:endpoint', endpoint, target, propertyName);
6 };
7}
8
9function auth(roles: string[]) {
10 return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
11 const originalMethod = descriptor.value;
12
13 descriptor.value = function (...args: any[]) {
14 // Simulate auth check
15 const userRoles = this.getCurrentUserRoles?.() || [];
16 const hasPermission = roles.some(role => userRoles.includes(role));
17
18 if (!hasPermission) {
19 throw new Error('Insufficient permissions');
20 }
21
22 return originalMethod.apply(this, args);
23 };
24 };
25}
26
27function rateLimit(maxRequests: number, windowMs: number) {
28 const requests = new Map<string, number[]>();
29
30 return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
31 const originalMethod = descriptor.value;
32
33 descriptor.value = function (...args: any[]) {
34 const clientId = this.getClientId?.() || 'default';
35 const now = Date.now();
36 const userRequests = requests.get(clientId) || [];
37
38 // Clean old requests
39 const validRequests = userRequests.filter(time => now - time < windowMs);
40
41 if (validRequests.length >= maxRequests) {
42 throw new Error('Rate limit exceeded');
43 }
44
45 validRequests.push(now);
46 requests.set(clientId, validRequests);
47
48 return originalMethod.apply(this, args);
49 };
50 };
51}
52
53class UserController {
54 @api('/users')
55 @auth(['admin', 'user'])
56 @rateLimit(100, 60000) // 100 requests per minute
57 @cache(30000)
58 async getUsers() {
59 return [
60 { id: '1', name: 'John' },
61 { id: '2', name: 'Jane' }
62 ];
63 }
64
65 @api('/users/:id')
66 @auth(['admin'])
67 @rateLimit(10, 60000) // 10 requests per minute for admin operations
68 async deleteUser(id: string) {
69 return { success: true, deletedId: id };
70 }
71
72 getCurrentUserRoles() {
73 return ['admin']; // Simplified
74 }
75
76 getClientId() {
77 return 'client-123'; // Simplified
78 }
79}TypeScript's metadata reflection capabilities enable runtime access to design-time type information, powering advanced frameworks and libraries.
Reflect Metadata Basics:
1// Install reflect-metadata: npm install reflect-metadata
2import 'reflect-metadata';
3
4// Basic metadata operations
5function entity(tableName: string) {
6 return function <T extends { new (...args: any[]): {} }>(constructor: T) {
7 Reflect.defineMetadata('tableName', tableName, constructor);
8 return constructor;
9 };
10}
11
12function column(options: { type?: string; nullable?: boolean; primary?: boolean } = {}) {
13 return function (target: any, propertyName: string) {
14 const columns = Reflect.getMetadata('columns', target) || [];
15 columns.push({
16 name: propertyName,
17 ...options
18 });
19 Reflect.defineMetadata('columns', columns, target);
20 };
21}
22
23@entity('users')
24class User {
25 @column({ type: 'varchar', primary: true })
26 id: string;
27
28 @column({ type: 'varchar' })
29 name: string;
30
31 @column({ type: 'varchar' })
32 email: string;
33
34 @column({ type: 'int', nullable: true })
35 age?: number;
36}
37
38// Extract metadata
39function getEntityMetadata(constructor: Function) {
40 return {
41 tableName: Reflect.getMetadata('tableName', constructor),
42 columns: Reflect.getMetadata('columns', constructor.prototype) || []
43 };
44}
45
46const userMetadata = getEntityMetadata(User);
47console.log(userMetadata);
48// Output: { tableName: 'users', columns: [{ name: 'id', type: 'varchar', primary: true }, ...] }Design-Time Type Metadata:
1// TypeScript emits design-time type information when emitDecoratorMetadata is enabled
2function logTypeMetadata(target: any, propertyName: string, descriptor?: PropertyDescriptor) {
3 // Get parameter types
4 const paramTypes = Reflect.getMetadata('design:paramtypes', target, propertyName);
5
6 // Get return type
7 const returnType = Reflect.getMetadata('design:returntype', target, propertyName);
8
9 // Get property type (for property decorators)
10 const type = Reflect.getMetadata('design:type', target, propertyName);
11
12 console.log(`Property: ${propertyName}`);
13 console.log('Parameter types:', paramTypes);
14 console.log('Return type:', returnType);
15 console.log('Property type:', type);
16}
17
18class TypeExample {
19 @logTypeMetadata
20 name: string;
21
22 @logTypeMetadata
23 calculateAge(birthDate: Date, currentDate: Date): number {
24 return currentDate.getFullYear() - birthDate.getFullYear();
25 }
26}
27
28// Advanced type reflection
29function getPropertyTypes(target: any): Map<string, Function> {
30 const propertyTypes = new Map<string, Function>();
31 const propertyNames = Object.getOwnPropertyNames(target.prototype);
32
33 for (const propertyName of propertyNames) {
34 const type = Reflect.getMetadata('design:type', target.prototype, propertyName);
35 if (type) {
36 propertyTypes.set(propertyName, type);
37 }
38 }
39
40 return propertyTypes;
41}
42
43function getMethodSignatures(target: any): Map<string, { params: Function[]; return: Function }> {
44 const signatures = new Map();
45 const propertyNames = Object.getOwnPropertyNames(target.prototype);
46
47 for (const propertyName of propertyNames) {
48 const paramTypes = Reflect.getMetadata('design:paramtypes', target.prototype, propertyName);
49 const returnType = Reflect.getMetadata('design:returntype', target.prototype, propertyName);
50
51 if (paramTypes || returnType) {
52 signatures.set(propertyName, {
53 params: paramTypes || [],
54 return: returnType
55 });
56 }
57 }
58
59 return signatures;
60}Advanced ORM with Metadata:
1// Relationship decorators
2function oneToMany(target: () => Function, foreignKey: string) {
3 return function (sourceTarget: any, propertyName: string) {
4 const relationships = Reflect.getMetadata('relationships', sourceTarget) || [];
5 relationships.push({
6 type: 'oneToMany',
7 property: propertyName,
8 target: target(),
9 foreignKey
10 });
11 Reflect.defineMetadata('relationships', relationships, sourceTarget);
12 };
13}
14
15function manyToOne(target: () => Function, foreignKey: string) {
16 return function (sourceTarget: any, propertyName: string) {
17 const relationships = Reflect.getMetadata('relationships', sourceTarget) || [];
18 relationships.push({
19 type: 'manyToOne',
20 property: propertyName,
21 target: target(),
22 foreignKey
23 });
24 Reflect.defineMetadata('relationships', relationships, sourceTarget);
25 };
26}
27
28// Index and constraint decorators
29function index(name?: string, unique?: boolean) {
30 return function (target: any, propertyName: string) {
31 const indexes = Reflect.getMetadata('indexes', target) || [];
32 indexes.push({
33 name: name || `idx_${propertyName}`,
34 columns: [propertyName],
35 unique: unique || false
36 });
37 Reflect.defineMetadata('indexes', indexes, target);
38 };
39}
40
41// Validation decorators with metadata
42function length(min: number, max: number) {
43 return function (target: any, propertyName: string) {
44 const validations = Reflect.getMetadata('validations', target) || [];
45 validations.push({
46 property: propertyName,
47 type: 'length',
48 min,
49 max
50 });
51 Reflect.defineMetadata('validations', validations, target);
52 };
53}
54
55@entity('posts')
56class Post {
57 @column({ type: 'varchar', primary: true })
58 id: string;
59
60 @column({ type: 'varchar' })
61 @index('title_idx')
62 @length(1, 200)
63 title: string;
64
65 @column({ type: 'text' })
66 @length(1, 10000)
67 content: string;
68
69 @column({ type: 'varchar' })
70 @index('author_idx')
71 authorId: string;
72
73 @manyToOne(() => User, 'authorId')
74 author: User;
75}
76
77@entity('users')
78class ExtendedUser extends User {
79 @oneToMany(() => Post, 'authorId')
80 posts: Post[];
81}
82
83// ORM Query Builder using metadata
84class QueryBuilder<T> {
85 private entity: Function;
86 private metadata: any;
87
88 constructor(entity: Function) {
89 this.entity = entity;
90 this.metadata = getEntityMetadata(entity);
91 }
92
93 select(): QueryBuilder<T> {
94 const tableName = this.metadata.tableName;
95 const columns = this.metadata.columns.map((col: any) => col.name).join(', ');
96 console.log(`SELECT ${columns} FROM ${tableName}`);
97 return this;
98 }
99
100 where(conditions: Partial<T>): QueryBuilder<T> {
101 const tableName = this.metadata.tableName;
102 const whereClause = Object.keys(conditions)
103 .map(key => `${key} = ?`)
104 .join(' AND ');
105 console.log(`WHERE ${whereClause}`);
106 return this;
107 }
108
109 join(property: keyof T): QueryBuilder<T> {
110 const relationships = Reflect.getMetadata('relationships', this.entity.prototype) || [];
111 const relationship = relationships.find((rel: any) => rel.property === property);
112
113 if (relationship) {
114 const targetMetadata = getEntityMetadata(relationship.target);
115 console.log(`JOIN ${targetMetadata.tableName} ON ${this.metadata.tableName}.${relationship.foreignKey} = ${targetMetadata.tableName}.id`);
116 }
117
118 return this;
119 }
120
121 async execute(): Promise<T[]> {
122 console.log('Executing query...');
123 return []; // Simplified
124 }
125}
126
127// Usage
128const userQuery = new QueryBuilder(ExtendedUser)
129 .select()
130 .join('posts')
131 .where({ name: 'John' })
132 .execute();Dependency Injection with Metadata:
1// Service registration with metadata
2function service(token?: string) {
3 return function <T extends { new (...args: any[]): {} }>(constructor: T) {
4 const serviceToken = token || constructor.name;
5 Reflect.defineMetadata('service:token', serviceToken, constructor);
6 Reflect.defineMetadata('service:dependencies',
7 Reflect.getMetadata('design:paramtypes', constructor) || [],
8 constructor
9 );
10
11 // Register with DI container
12 DIContainer.register(serviceToken, constructor);
13 return constructor;
14 };
15}
16
17function injectable(token: string) {
18 return function (target: any, propertyName: string | undefined, parameterIndex: number) {
19 const existingTokens = Reflect.getMetadata('inject:tokens', target) || [];
20 existingTokens[parameterIndex] = token;
21 Reflect.defineMetadata('inject:tokens', existingTokens, target);
22 };
23}
24
25// Advanced DI Container with metadata
26class AdvancedDIContainer {
27 private static services = new Map<string, any>();
28 private static instances = new Map<string, any>();
29
30 static register(token: string, constructor: Function) {
31 this.services.set(token, constructor);
32 }
33
34 static resolve<T>(token: string): T {
35 if (this.instances.has(token)) {
36 return this.instances.get(token);
37 }
38
39 const ServiceClass = this.services.get(token);
40 if (!ServiceClass) {
41 throw new Error(`Service '${token}' not found`);
42 }
43
44 // Get dependency tokens from metadata
45 const dependencyTokens = Reflect.getMetadata('inject:tokens', ServiceClass) || [];
46 const dependencyTypes = Reflect.getMetadata('design:paramtypes', ServiceClass) || [];
47
48 // Resolve dependencies
49 const dependencies = dependencyTokens.map((depToken: string, index: number) => {
50 if (depToken) {
51 return this.resolve(depToken);
52 }
53
54 // Try to resolve by type name
55 const dependencyType = dependencyTypes[index];
56 if (dependencyType) {
57 const typeToken = Reflect.getMetadata('service:token', dependencyType);
58 if (typeToken) {
59 return this.resolve(typeToken);
60 }
61 }
62
63 throw new Error(`Cannot resolve dependency at index ${index}`);
64 });
65
66 const instance = new ServiceClass(...dependencies);
67 this.instances.set(token, instance);
68 return instance;
69 }
70}
71
72@service('logger')
73class Logger {
74 log(message: string) {
75 console.log(`[LOG] ${message}`);
76 }
77}
78
79@service('database')
80class Database {
81 constructor(@injectable('logger') private logger: Logger) {}
82
83 query(sql: string) {
84 this.logger.log(`Executing query: ${sql}`);
85 return [];
86 }
87}
88
89@service('user-service')
90class UserServiceWithDI {
91 constructor(
92 @injectable('database') private db: Database,
93 @injectable('logger') private logger: Logger
94 ) {}
95
96 async createUser(userData: any) {
97 this.logger.log('Creating new user');
98 return this.db.query('INSERT INTO users...');
99 }
100}
101
102// Usage
103const userService = AdvancedDIContainer.resolve<UserServiceWithDI>('user-service');Runtime Schema Validation:
1// Schema validation using metadata
2function validate(target: any): boolean {
3 const validations = Reflect.getMetadata('validations', target) || [];
4
5 for (const validation of validations) {
6 const value = target[validation.property];
7
8 switch (validation.type) {
9 case 'length':
10 if (typeof value === 'string' &&
11 (value.length < validation.min || value.length > validation.max)) {
12 throw new Error(`${validation.property} length must be between ${validation.min} and ${validation.max}`);
13 }
14 break;
15
16 case 'range':
17 if (typeof value === 'number' &&
18 (value < validation.min || value > validation.max)) {
19 throw new Error(`${validation.property} must be between ${validation.min} and ${validation.max}`);
20 }
21 break;
22 }
23 }
24
25 return true;
26}
27
28// Auto-validation decorator
29function autoValidate(target: any, propertyName: string, descriptor: PropertyDescriptor) {
30 const originalMethod = descriptor.value;
31
32 descriptor.value = function (...args: any[]) {
33 // Validate all arguments that have validation metadata
34 for (const arg of args) {
35 if (arg && typeof arg === 'object') {
36 validate(arg);
37 }
38 }
39
40 return originalMethod.apply(this, args);
41 };
42}
43
44class ValidatedUserService {
45 @autoValidate
46 updateUser(userData: User) {
47 console.log('Updating user with validated data');
48 return userData;
49 }
50}Combine decorator patterns to create sophisticated systems for validation, caching, security, and framework development.
Comprehensive Validation System:
1// Validation rule types
2interface ValidationRule {
3 type: string;
4 message?: string;
5 [key: string]: any;
6}
7
8// Validation decorators
9function required(message?: string) {
10 return function (target: any, propertyName: string) {
11 addValidationRule(target, propertyName, {
12 type: 'required',
13 message: message || `${propertyName} is required`
14 });
15 };
16}
17
18function minLength(min: number, message?: string) {
19 return function (target: any, propertyName: string) {
20 addValidationRule(target, propertyName, {
21 type: 'minLength',
22 min,
23 message: message || `${propertyName} must be at least ${min} characters`
24 });
25 };
26}
27
28function email(message?: string) {
29 return function (target: any, propertyName: string) {
30 addValidationRule(target, propertyName, {
31 type: 'email',
32 pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
33 message: message || `${propertyName} must be a valid email`
34 });
35 };
36}
37
38function range(min: number, max: number, message?: string) {
39 return function (target: any, propertyName: string) {
40 addValidationRule(target, propertyName, {
41 type: 'range',
42 min,
43 max,
44 message: message || `${propertyName} must be between ${min} and ${max}`
45 });
46 };
47}
48
49function custom(validator: (value: any) => boolean, message: string) {
50 return function (target: any, propertyName: string) {
51 addValidationRule(target, propertyName, {
52 type: 'custom',
53 validator,
54 message
55 });
56 };
57}
58
59function addValidationRule(target: any, propertyName: string, rule: ValidationRule) {
60 const rules = Reflect.getMetadata('validation:rules', target) || {};
61 if (!rules[propertyName]) {
62 rules[propertyName] = [];
63 }
64 rules[propertyName].push(rule);
65 Reflect.defineMetadata('validation:rules', rules, target);
66}
67
68// Validation engine
69class ValidationEngine {
70 static validate(instance: any): { valid: boolean; errors: Record<string, string[]> } {
71 const rules = Reflect.getMetadata('validation:rules', instance) || {};
72 const errors: Record<string, string[]> = {};
73
74 for (const [propertyName, propertyRules] of Object.entries(rules)) {
75 const value = instance[propertyName];
76 const propertyErrors: string[] = [];
77
78 for (const rule of propertyRules as ValidationRule[]) {
79 const error = this.validateRule(value, rule);
80 if (error) {
81 propertyErrors.push(error);
82 }
83 }
84
85 if (propertyErrors.length > 0) {
86 errors[propertyName] = propertyErrors;
87 }
88 }
89
90 return {
91 valid: Object.keys(errors).length === 0,
92 errors
93 };
94 }
95
96 private static validateRule(value: any, rule: ValidationRule): string | null {
97 switch (rule.type) {
98 case 'required':
99 return (value === undefined || value === null || value === '') ? rule.message : null;
100
101 case 'minLength':
102 return (typeof value === 'string' && value.length < rule.min) ? rule.message : null;
103
104 case 'email':
105 return (typeof value === 'string' && !rule.pattern.test(value)) ? rule.message : null;
106
107 case 'range':
108 return (typeof value === 'number' && (value < rule.min || value > rule.max)) ? rule.message : null;
109
110 case 'custom':
111 return !rule.validator(value) ? rule.message : null;
112
113 default:
114 return null;
115 }
116 }
117}
118
119// Auto-validation decorator
120function validated(target: any, propertyName: string, descriptor: PropertyDescriptor) {
121 const originalMethod = descriptor.value;
122
123 descriptor.value = function (...args: any[]) {
124 // Validate each argument that has validation metadata
125 for (let i = 0; i < args.length; i++) {
126 const arg = args[i];
127 if (arg && typeof arg === 'object') {
128 const validation = ValidationEngine.validate(arg);
129 if (!validation.valid) {
130 throw new Error(`Validation failed: ${JSON.stringify(validation.errors)}`);
131 }
132 }
133 }
134
135 return originalMethod.apply(this, args);
136 };
137}
138
139// Usage example
140class CreateUserRequest {
141 @required()
142 @minLength(2)
143 name: string;
144
145 @required()
146 @email()
147 email: string;
148
149 @range(13, 120)
150 age: number;
151
152 @custom((value) => value !== 'admin', 'Username cannot be "admin"')
153 username: string;
154}
155
156class UserController {
157 @validated
158 createUser(request: CreateUserRequest) {
159 console.log('Creating user:', request);
160 return { id: Date.now().toString(), ...request };
161 }
162}Advanced Caching System:
1// Cache configuration
2interface CacheConfig {
3 ttl?: number;
4 key?: string | ((args: any[]) => string);
5 invalidateOn?: string[];
6 storage?: 'memory' | 'redis' | 'localStorage';
7}
8
9// Cache storage implementations
10interface CacheStorage {
11 get(key: string): Promise<any>;
12 set(key: string, value: any, ttl?: number): Promise<void>;
13 delete(key: string): Promise<void>;
14 clear(): Promise<void>;
15}
16
17class MemoryStorage implements CacheStorage {
18 private cache = new Map<string, { value: any; expires: number }>();
19
20 async get(key: string): Promise<any> {
21 const item = this.cache.get(key);
22 if (!item) return undefined;
23
24 if (Date.now() > item.expires) {
25 this.cache.delete(key);
26 return undefined;
27 }
28
29 return item.value;
30 }
31
32 async set(key: string, value: any, ttl: number = 60000): Promise<void> {
33 this.cache.set(key, {
34 value,
35 expires: Date.now() + ttl
36 });
37 }
38
39 async delete(key: string): Promise<void> {
40 this.cache.delete(key);
41 }
42
43 async clear(): Promise<void> {
44 this.cache.clear();
45 }
46}
47
48// Cache manager
49class CacheManager {
50 private static storages = new Map<string, CacheStorage>();
51 private static invalidationGroups = new Map<string, Set<string>>();
52
53 static registerStorage(name: string, storage: CacheStorage) {
54 this.storages.set(name, storage);
55 }
56
57 static getStorage(name: string): CacheStorage {
58 const storage = this.storages.get(name);
59 if (!storage) {
60 throw new Error(`Storage '${name}' not found`);
61 }
62 return storage;
63 }
64
65 static addToInvalidationGroup(group: string, key: string) {
66 if (!this.invalidationGroups.has(group)) {
67 this.invalidationGroups.set(group, new Set());
68 }
69 this.invalidationGroups.get(group)!.add(key);
70 }
71
72 static async invalidateGroup(group: string) {
73 const keys = this.invalidationGroups.get(group);
74 if (!keys) return;
75
76 const promises: Promise<void>[] = [];
77 for (const [storageName, storage] of this.storages) {
78 for (const key of keys) {
79 promises.push(storage.delete(key));
80 }
81 }
82
83 await Promise.all(promises);
84 this.invalidationGroups.delete(group);
85 }
86}
87
88// Initialize default storage
89CacheManager.registerStorage('memory', new MemoryStorage());
90
91// Cache decorators
92function cacheable(config: CacheConfig = {}) {
93 return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
94 const originalMethod = descriptor.value;
95 const storage = CacheManager.getStorage(config.storage || 'memory');
96
97 descriptor.value = async function (...args: any[]) {
98 // Generate cache key
99 let cacheKey: string;
100 if (typeof config.key === 'function') {
101 cacheKey = config.key(args);
102 } else if (typeof config.key === 'string') {
103 cacheKey = config.key;
104 } else {
105 cacheKey = `${target.constructor.name}:${propertyName}:${JSON.stringify(args)}`;
106 }
107
108 // Try to get from cache
109 const cached = await storage.get(cacheKey);
110 if (cached !== undefined) {
111 console.log(`Cache hit for ${cacheKey}`);
112 return cached;
113 }
114
115 // Execute original method
116 const result = await originalMethod.apply(this, args);
117
118 // Store in cache
119 await storage.set(cacheKey, result, config.ttl);
120
121 // Add to invalidation groups
122 if (config.invalidateOn) {
123 for (const group of config.invalidateOn) {
124 CacheManager.addToInvalidationGroup(group, cacheKey);
125 }
126 }
127
128 console.log(`Cache miss for ${cacheKey}, result cached`);
129 return result;
130 };
131 };
132}
133
134function invalidateCache(groups: string[]) {
135 return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
136 const originalMethod = descriptor.value;
137
138 descriptor.value = async function (...args: any[]) {
139 const result = await originalMethod.apply(this, args);
140
141 // Invalidate cache groups
142 for (const group of groups) {
143 await CacheManager.invalidateGroup(group);
144 }
145
146 return result;
147 };
148 };
149}
150
151// Usage example
152class ProductService {
153 @cacheable({
154 ttl: 300000, // 5 minutes
155 key: (args) => `product:${args[0]}`,
156 invalidateOn: ['products']
157 })
158 async getProduct(id: string) {
159 console.log(`Fetching product ${id} from database`);
160 // Simulate database call
161 await new Promise(resolve => setTimeout(resolve, 100));
162 return { id, name: `Product ${id}`, price: Math.random() * 100 };
163 }
164
165 @cacheable({
166 ttl: 60000, // 1 minute
167 invalidateOn: ['products']
168 })
169 async getAllProducts() {
170 console.log('Fetching all products from database');
171 await new Promise(resolve => setTimeout(resolve, 200));
172 return [
173 { id: '1', name: 'Product 1' },
174 { id: '2', name: 'Product 2' }
175 ];
176 }
177
178 @invalidateCache(['products'])
179 async updateProduct(id: string, data: any) {
180 console.log(`Updating product ${id}`);
181 return { id, ...data, updatedAt: new Date() };
182 }
183}Security and Authorization System:
1// Role and permission system
2interface Principal {
3 id: string;
4 roles: string[];
5 permissions: string[];
6}
7
8class SecurityContext {
9 private static current: Principal | null = null;
10
11 static setCurrent(principal: Principal) {
12 this.current = principal;
13 }
14
15 static getCurrent(): Principal | null {
16 return this.current;
17 }
18
19 static hasRole(role: string): boolean {
20 return this.current?.roles.includes(role) || false;
21 }
22
23 static hasPermission(permission: string): boolean {
24 return this.current?.permissions.includes(permission) || false;
25 }
26
27 static hasAnyRole(roles: string[]): boolean {
28 return roles.some(role => this.hasRole(role));
29 }
30
31 static hasAllRoles(roles: string[]): boolean {
32 return roles.every(role => this.hasRole(role));
33 }
34}
35
36// Security decorators
37function requireRoles(...roles: string[]) {
38 return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
39 const originalMethod = descriptor.value;
40
41 descriptor.value = function (...args: any[]) {
42 if (!SecurityContext.hasAnyRole(roles)) {
43 throw new Error(`Access denied. Required roles: ${roles.join(', ')}`);
44 }
45
46 return originalMethod.apply(this, args);
47 };
48 };
49}
50
51function requirePermissions(...permissions: string[]) {
52 return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
53 const originalMethod = descriptor.value;
54
55 descriptor.value = function (...args: any[]) {
56 for (const permission of permissions) {
57 if (!SecurityContext.hasPermission(permission)) {
58 throw new Error(`Access denied. Required permission: ${permission}`);
59 }
60 }
61
62 return originalMethod.apply(this, args);
63 };
64 };
65}
66
67function audit(action: string) {
68 return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
69 const originalMethod = descriptor.value;
70
71 descriptor.value = function (...args: any[]) {
72 const principal = SecurityContext.getCurrent();
73 const timestamp = new Date().toISOString();
74
75 console.log(`[AUDIT] ${timestamp} - User ${principal?.id || 'anonymous'} performed ${action}`);
76
77 try {
78 const result = originalMethod.apply(this, args);
79 console.log(`[AUDIT] ${timestamp} - Action ${action} completed successfully`);
80 return result;
81 } catch (error) {
82 console.log(`[AUDIT] ${timestamp} - Action ${action} failed: ${error.message}`);
83 throw error;
84 }
85 };
86 };
87}
88
89// Usage example
90class AdminController {
91 @requireRoles('admin')
92 @audit('delete_user')
93 deleteUser(userId: string) {
94 console.log(`Deleting user ${userId}`);
95 return { success: true };
96 }
97
98 @requirePermissions('user:read', 'user:write')
99 @audit('update_user')
100 updateUser(userId: string, data: any) {
101 console.log(`Updating user ${userId}`);
102 return { id: userId, ...data };
103 }
104
105 @requireRoles('admin', 'moderator')
106 @cacheable({ ttl: 60000 })
107 @audit('view_admin_dashboard')
108 getAdminDashboard() {
109 return {
110 userCount: 1000,
111 activeUsers: 250,
112 revenue: 50000
113 };
114 }
115}
116
117// Set security context
118SecurityContext.setCurrent({
119 id: 'admin-user-1',
120 roles: ['admin'],
121 permissions: ['user:read', 'user:write', 'user:delete']
122});Framework Integration Example:
1// HTTP framework integration
2function controller(basePath: string) {
3 return function <T extends { new (...args: any[]): {} }>(constructor: T) {
4 Reflect.defineMetadata('controller:path', basePath, constructor);
5
6 // Register controller with framework
7 HttpFramework.registerController(constructor);
8
9 return constructor;
10 };
11}
12
13function get(path: string) {
14 return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
15 Reflect.defineMetadata('route:method', 'GET', target, propertyName);
16 Reflect.defineMetadata('route:path', path, target, propertyName);
17 };
18}
19
20function post(path: string) {
21 return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
22 Reflect.defineMetadata('route:method', 'POST', target, propertyName);
23 Reflect.defineMetadata('route:path', path, target, propertyName);
24 };
25}
26
27function body(target: any, propertyName: string, parameterIndex: number) {
28 const paramTypes = Reflect.getMetadata('route:paramTypes', target, propertyName) || [];
29 paramTypes[parameterIndex] = 'body';
30 Reflect.defineMetadata('route:paramTypes', paramTypes, target, propertyName);
31}
32
33function param(name: string) {
34 return function (target: any, propertyName: string, parameterIndex: number) {
35 const paramTypes = Reflect.getMetadata('route:paramTypes', target, propertyName) || [];
36 paramTypes[parameterIndex] = { type: 'param', name };
37 Reflect.defineMetadata('route:paramTypes', paramTypes, target, propertyName);
38 };
39}
40
41// Simple HTTP framework
42class HttpFramework {
43 private static controllers: Function[] = [];
44
45 static registerController(controller: Function) {
46 this.controllers.push(controller);
47 }
48
49 static start() {
50 console.log('Starting HTTP framework...');
51
52 for (const Controller of this.controllers) {
53 const basePath = Reflect.getMetadata('controller:path', Controller);
54 const instance = new Controller();
55
56 const methods = Object.getOwnPropertyNames(Controller.prototype);
57
58 for (const methodName of methods) {
59 if (methodName === 'constructor') continue;
60
61 const httpMethod = Reflect.getMetadata('route:method', Controller.prototype, methodName);
62 const routePath = Reflect.getMetadata('route:path', Controller.prototype, methodName);
63
64 if (httpMethod && routePath) {
65 const fullPath = basePath + routePath;
66 console.log(`Registered ${httpMethod} ${fullPath} -> ${Controller.name}.${methodName}`);
67 }
68 }
69 }
70 }
71}
72
73// Usage example
74@controller('/api/users')
75class UserApiController {
76 @get('/')
77 @cacheable({ ttl: 30000 })
78 getUsers() {
79 return [
80 { id: '1', name: 'John' },
81 { id: '2', name: 'Jane' }
82 ];
83 }
84
85 @get('/:id')
86 @cacheable({ ttl: 60000, key: (args) => `user:${args[0]}` })
87 getUser(@param('id') id: string) {
88 return { id, name: `User ${id}` };
89 }
90
91 @post('/')
92 @validated
93 @audit('create_user')
94 @invalidateCache(['users'])
95 createUser(@body userData: CreateUserRequest) {
96 return { id: Date.now().toString(), ...userData };
97 }
98}
99
100// Start the framework
101HttpFramework.start();Your progress is saved in this browser, on this device only.