English

Explore the power of TypeScript Decorators for metadata programming, aspect-oriented programming, and enhancing code with declarative patterns. A comprehensive guide for global developers.

TypeScript Decorators: Mastering Metadata Programming Patterns for Robust Applications

In the vast landscape of modern software development, maintaining clean, scalable, and manageable codebases is paramount. TypeScript, with its powerful type system and advanced features, provides developers with tools to achieve this. Among its most intriguing and transformative features are Decorators. While still an experimental feature at the time of writing (Stage 3 proposal for ECMAScript), decorators are widely used in frameworks like Angular and TypeORM, fundamentally changing how we approach design patterns, metadata programming, and aspect-oriented programming (AOP).

This comprehensive guide will delve deep into TypeScript decorators, exploring their mechanics, various types, practical applications, and best practices. Whether you're building large-scale enterprise applications, microservices, or client-side web interfaces, understanding decorators will empower you to write more declarative, maintainable, and powerful TypeScript code.

Understanding the Core Concept: What is a Decorator?

At its heart, a decorator is a special kind of declaration that can be attached to a class declaration, method, accessor, property, or parameter. Decorators are functions that return a new value (or modify an existing one) for the target they are decorating. Their primary purpose is to add metadata or change the behavior of the declaration they are attached to, without modifying the underlying code structure directly. This external, declarative way of augmenting code is incredibly powerful.

Think of decorators as annotations or labels that you apply to parts of your code. These labels can then be read or acted upon by other parts of your application or by frameworks, often at runtime, to provide additional functionality or configuration.

The Syntax of a Decorator

Decorators are prefixed with an @ symbol, followed by the decorator function's name. They are placed immediately before the declaration they are decorating.

@MyDecorator\nclass MyClass {\n  @AnotherDecorator\n  myMethod() {\n    // ...\n  }\n}

Enabling Decorators in TypeScript

Before you can use decorators, you must enable the experimentalDecorators compiler option in your tsconfig.json file. Additionally, for advanced metadata reflection capabilities (often used by frameworks), you'll also need emitDecoratorMetadata and the reflect-metadata polyfill.

// tsconfig.json\n{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"module\": \"commonjs\",\n    \"experimentalDecorators\": true,\n    \"emitDecoratorMetadata\": true,\n    \"outDir\": \"./dist\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true\n  }\n}

You also need to install reflect-metadata:

npm install reflect-metadata --save\n# or\nyarn add reflect-metadata

And import it at the very top of your application's entry point (e.g., main.ts or app.ts):

import \"reflect-metadata\";\n// Your application code follows

Decorator Factories: Customization at Your Fingertips

While a basic decorator is a function, often you'll need to pass arguments to a decorator to configure its behavior. This is achieved by using a decorator factory. A decorator factory is a function that returns the actual decorator function. When you apply a decorator factory, you call it with its arguments, and it then returns the decorator function that TypeScript applies to your code.

Creating a Simple Decorator Factory Example

Let's create a factory for a Logger decorator that can log messages with different prefixes.

function Logger(prefix: string) {\n  return function (target: Function) {\n    console.log(`[${prefix}] Class ${target.name} has been defined.`);\n  };\n}\n\n@Logger(\"APP_INIT\")\nclass ApplicationBootstrap {\n  constructor() {\n    console.log(\"Application is starting...\");\n  }\n}\n\nconst app = new ApplicationBootstrap();\n// Output:\n// [APP_INIT] Class ApplicationBootstrap has been defined.\n// Application is starting...

In this example, Logger("APP_INIT") is the decorator factory call. It returns the actual decorator function which takes target: Function (the class constructor) as its argument. This allows for dynamic configuration of the decorator's behavior.

Types of Decorators in TypeScript

TypeScript supports five distinct types of decorators, each applicable to a specific kind of declaration. The signature of the decorator function varies based on the context it's applied to.

1. Class Decorators

Class decorators are applied to class declarations. The decorator function receives the constructor of the class as its only argument. A class decorator can observe, modify, or even replace a class definition.

Signature:

function ClassDecorator(target: Function) { ... }

Return Value:

If the class decorator returns a value, it will replace the class declaration with the provided constructor function. This is a powerful feature, often used for mixins or class augmentation. If no value is returned, the original class is used.

Use Cases:

Class Decorator Example: Injecting a Service

Imagine a simple dependency injection scenario where you want to mark a class as "injectable" and optionally provide a name for it in a container.

const InjectableServiceRegistry = new Map<string, Function>();\n\nfunction Injectable(name?: string) {\n  return function<T extends { new(...args: any[]): {} }>(constructor: T) {\n    const serviceName = name || constructor.name;\n    InjectableServiceRegistry.set(serviceName, constructor);\n    console.log(`Registered service: ${serviceName}`);\n\n    // Optionally, you could return a new class here to augment behavior\n    return class extends constructor {\n      createdAt = new Date();\n      // Additional properties or methods for all injected services\n    };\n  };\n}\n\n@Injectable(\"UserService\")\nclass UserDataService {\n  getUsers() {\n    return [{ id: 1, name: \"Alice\" }, { id: 2, name: \"Bob\" }];\n  }\n}\n\n@Injectable()\nclass ProductDataService {\n  getProducts() {\n    return [{ id: 101, name: \"Laptop\" }, { id: 102, name: \"Mouse\" }];\n  }\n}\n\nconsole.log(\"--- Services Registered ---\");\nconsole.log(Array.from(InjectableServiceRegistry.keys()));\n\nconst userServiceConstructor = InjectableServiceRegistry.get(\"UserService\");\nif (userServiceConstructor) {\n  const userServiceInstance = new userServiceConstructor();\n  console.log(\"Users:\", userServiceInstance.getUsers());\n  // console.log(\"User Service Created At:\", userServiceInstance.createdAt); // If the returned class is used\n}

This example demonstrates how a class decorator can register a class and even modify its constructor. The Injectable decorator makes the class discoverable by a theoretical dependency injection system.

2. Method Decorators

Method decorators are applied to method declarations. They receive three arguments: the target object (for static members, the constructor function; for instance members, the prototype of the class), the name of the method, and the property descriptor of the method.

Signature:

function MethodDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }

Return Value:

A method decorator can return a new PropertyDescriptor. If it does, this descriptor will be used to define the method. This allows you to modify or replace the original method's implementation, making it incredibly powerful for AOP.

Use Cases:

Method Decorator Example: Performance Monitoring

Let's create a MeasurePerformance decorator to log the execution time of a method.

function MeasurePerformance(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {\n  const originalMethod = descriptor.value;\n\n  descriptor.value = function(...args: any[]) {\n    const start = process.hrtime.bigint();\n    const result = originalMethod.apply(this, args);\n    const end = process.hrtime.bigint();\n    const duration = Number(end - start) / 1_000_000;\n    console.log(`Method \"${propertyKey}\" executed in ${duration.toFixed(2)} ms`);\n    return result;\n  };\n\n  return descriptor;\n}\n\nclass DataProcessor {\n  @MeasurePerformance\n  processData(data: number[]): number[] {\n    // Simulate a complex, time-consuming operation\n    for (let i = 0; i < 1_000_000; i++) {\n      Math.sin(i);\n    }\n    return data.map(n => n * 2);\n  }\n\n  @MeasurePerformance\n  fetchRemoteData(id: string): Promise<string> {\n    return new Promise(resolve => {\n      setTimeout(() => {\n        resolve(`Data for ID: ${id}`);\n      }, 500);\n    });\n  }\n}\n\nconst processor = new DataProcessor();\nprocessor.processData([1, 2, 3]);\nprocessor.fetchRemoteData(\"abc\").then(result => console.log(result));

The MeasurePerformance decorator wraps the original method with timing logic, printing the execution duration without cluttering the business logic within the method itself. This is a classic example of Aspect-Oriented Programming (AOP).

3. Accessor Decorators

Accessor decorators are applied to accessor (get and set) declarations. Similar to method decorators, they receive the target object, the name of the accessor, and its property descriptor.

Signature:

function AccessorDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }

Return Value:

An accessor decorator can return a new PropertyDescriptor, which will be used to define the accessor.

Use Cases:

Accessor Decorator Example: Caching Getters

Let's create a decorator that caches the result of an expensive getter computation.

function CachedGetter(target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n  const originalGetter = descriptor.get;\n  const cacheKey = `_cached_${String(propertyKey)}`;\n\n  if (originalGetter) {\n    descriptor.get = function() {\n      if (this[cacheKey] === undefined) {\n        console.log(`[Cache Miss] Computing value for ${String(propertyKey)}`);\n        this[cacheKey] = originalGetter.apply(this);\n      } else {\n        console.log(`[Cache Hit] Using cached value for ${String(propertyKey)}`);\n      }\n      return this[cacheKey];\n    };\n  }\n  return descriptor;\n}\n\nclass ReportGenerator {\n  private data: number[];\n\n  constructor(data: number[]) {\n    this.data = data;\n  }\n\n  // Simulates an expensive computation\n  @CachedGetter\n  get expensiveSummary(): number {\n    console.log(\"Performing expensive summary calculation...\");\n    return this.data.reduce((sum, current) => sum + current, 0) / this.data.length;\n  }\n}\n\nconst generator = new ReportGenerator([10, 20, 30, 40, 50]);\n\nconsole.log(\"First access:\", generator.expensiveSummary);\nconsole.log(\"Second access:\", generator.expensiveSummary);\nconsole.log(\"Third access:\", generator.expensiveSummary);

This decorator ensures that the expensiveSummary getter's computation only runs once, subsequent calls return the cached value. This pattern is very useful for optimizing performance where property access involves heavy computation or external calls.

4. Property Decorators

Property decorators are applied to property declarations. They receive two arguments: the target object (for static members, the constructor function; for instance members, the prototype of the class), and the name of the property.

Signature:

function PropertyDecorator(target: Object, propertyKey: string | symbol) { ... }

Return Value:

Property decorators cannot return any value. Their primary use is to register metadata about the property. They cannot directly change the property's value or its descriptor at the time of decoration, as the descriptor for a property is not yet fully defined when property decorators are run.

Use Cases:

Property Decorator Example: Required Field Validation

Let's create a decorator to mark a property as "required" and then validate it at runtime.

interface ValidationRule {\n  property: string | symbol;\n  validate: (value: any) => boolean;\n  message: string;\n}\n\nconst validationRules: Map<Function, ValidationRule[]> = new Map();\n\nfunction Required(target: Object, propertyKey: string | symbol) {\n  const rules = validationRules.get(target.constructor) || [];\n  rules.push({\n    property: propertyKey,\n    validate: (value: any) => value !== null && value !== undefined && value !== \"\",\n    message: `${String(propertyKey)} is required.`\n  });\n  validationRules.set(target.constructor, rules);\n}\n\nfunction validate(instance: any): string[] {\n  const classRules = validationRules.get(instance.constructor) || [];\n  const errors: string[] = [];\n\n  for (const rule of classRules) {\n    if (!rule.validate(instance[rule.property])) {\n      errors.push(rule.message);\n    }\n  }\n  return errors;\n}\n\nclass UserProfile {\n  @Required\n  firstName: string;\n\n  @Required\n  lastName: string;\n\n  age?: number;\n\n  constructor(firstName: string, lastName: string, age?: number) {\n    this.firstName = firstName;\n    this.lastName = lastName;\n    this.age = age;\n  }\n}\n\nconst user1 = new UserProfile(\"John\", \"Doe\", 30);\nconsole.log(\"User 1 validation errors:\", validate(user1)); // []\n\nconst user2 = new UserProfile(\"\", \"Smith\");\nconsole.log(\"User 2 validation errors:\", validate(user2)); // [\"firstName is required.\"]\n\nconst user3 = new UserProfile(\"Alice\", \"\");\nconsole.log(\"User 3 validation errors:\", validate(user3)); // [\"lastName is required.\"]

The Required decorator simply registers the validation rule with a central validationRules map. A separate validate function then uses this metadata to check the instance at runtime. This pattern separates validation logic from data definition, making it reusable and clean.

5. Parameter Decorators

Parameter decorators are applied to parameters within a class constructor or a method. They receive three arguments: the target object (for static members, the constructor function; for instance members, the prototype of the class), the name of the method (or undefined for constructor parameters), and the ordinal index of the parameter in the function's parameter list.

Signature:

function ParameterDecorator(target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) { ... }

Return Value:

Parameter decorators cannot return any value. Like property decorators, their primary role is to add metadata about the parameter.

Use Cases:

Parameter Decorator Example: Injecting Request Data

Let's simulate how a web framework might use parameter decorators to inject specific data into a method parameter, such as a user ID from a request.

interface ParameterMetadata {\n  index: number;\n  key: string | symbol;\n  resolver: (request: any) => any;\n}\n\nconst parameterResolvers: Map<Function, Map<string | symbol, ParameterMetadata[]>> = new Map();\n\nfunction RequestParam(paramName: string) {\n  return function (target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) {\n    const targetKey = propertyKey || \"constructor\";\n    let methodResolvers = parameterResolvers.get(target.constructor);\n    if (!methodResolvers) {\n      methodResolvers = new Map();\n      parameterResolvers.set(target.constructor, methodResolvers);\n    }\n    const paramMetadata = methodResolvers.get(targetKey) || [];\n    paramMetadata.push({\n      index: parameterIndex,\n      key: targetKey,\n      resolver: (request: any) => request[paramName]\n    });\n    methodResolvers.set(targetKey, paramMetadata);\n  };\n}\n\n// A hypothetical framework function to invoke a method with resolved parameters\nfunction executeWithParams(instance: any, methodName: string, request: any) {\n  const classResolvers = parameterResolvers.get(instance.constructor);\n  if (!classResolvers) {\n    return (instance[methodName] as Function).apply(instance, []);\n  }\n  const methodParamMetadata = classResolvers.get(methodName);\n  if (!methodParamMetadata) {\n    return (instance[methodName] as Function).apply(instance, []);\n  }\n\n  const args: any[] = Array(methodParamMetadata.length);\n  for (const meta of methodParamMetadata) {\n    args[meta.index] = meta.resolver(request);\n  }\n  return (instance[methodName] as Function).apply(instance, args);\n}\n\nclass UserController {\n  getUser(@RequestParam(\"id\") userId: string, @RequestParam(\"token\") authToken?: string) {\n    console.log(`Fetching user with ID: ${userId}, Token: ${authToken || \"N/A\"}`);\n    return { id: userId, name: \"Jane Doe\" };\n  }\n\n  deleteUser(@RequestParam(\"id\") userId: string) {\n    console.log(`Deleting user with ID: ${userId}`);\n    return { status: \"deleted\", id: userId };\n  }\n}\n\nconst userController = new UserController();\n\n// Simulate an incoming request\nconst mockRequest = {\n  id: \"user123\",\n  token: \"abc-123\",\n  someOtherProp: \"xyz\"\n};\n\nconsole.log("\n--- Executing getUser ---");\nexecuteWithParams(userController, \"getUser\", mockRequest);\n\nconsole.log("\n--- Executing deleteUser ---");\nexecuteWithParams(userController, \"deleteUser\", { id: \"user456\" });

This example showcases how parameter decorators can gather information about required method parameters. A framework can then use this gathered metadata to automatically resolve and inject appropriate values when the method is called, significantly simplifying controller or service logic.

Decorator Composition and Execution Order

Decorators can be applied in various combinations, and understanding their execution order is crucial for predicting behavior and avoiding unexpected issues.

Multiple Decorators on a Single Target

When multiple decorators are applied to a single declaration (e.g., a class, method, or property), they execute in a specific order: from bottom to top, or right to left, for their evaluation. However, their results are applied in the opposite order.

@DecoratorA\n@DecoratorB\nclass MyClass {\n  // ...\n}\n

Here, DecoratorB will be evaluated first, then DecoratorA. If they modify the class (e.g., by returning a new constructor), the modification from DecoratorA will wrap or apply over the modification from DecoratorB.

Example: Chaining Method Decorators

Consider two method decorators: LogCall and Authorization.

function LogCall(target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n  const originalMethod = descriptor.value;\n  descriptor.value = function (...args: any[]) {\n    console.log(`[LOG] Calling ${String(propertyKey)} with args:`, args);\n    const result = originalMethod.apply(this, args);\n    console.log(`[LOG] Method ${String(propertyKey)} returned:`, result);\n    return result;\n  };\n  return descriptor;\n}\n\nfunction Authorization(roles: string[]) {\n  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n    const originalMethod = descriptor.value;\n    descriptor.value = function (...args: any[]) {\n      const currentUserRoles = [\"admin\"]; // Simulate fetching current user roles\n      const authorized = roles.some(role => currentUserRoles.includes(role));\n      if (!authorized) {\n        console.warn(`[AUTH] Access denied for ${String(propertyKey)}. Required roles: ${roles.join(\", \")}`);\n        throw new Error(\"Unauthorized access\");\n      }\n      console.log(`[AUTH] Access granted for ${String(propertyKey)}`);\n      return originalMethod.apply(this, args);\n    };\n    return descriptor;\n  };\n}\n\nclass SecureService {\n  @LogCall\n  @Authorization([\"admin\"])\n  deleteSensitiveData(id: string) {\n    console.log(`Deleting sensitive data for ID: ${id}`);\n    return `Data ID ${id} deleted.`;\n  }\n\n  @Authorization([\"user\"])\n  @LogCall // Order changed here\n  fetchPublicData(query: string) {\n    console.log(`Fetching public data with query: ${query}`);\n    return `Public data for query: ${query}`; \n  }\n}\n\nconst service = new SecureService();\n\ntry {\n  console.log("\n--- Calling deleteSensitiveData (Admin User) ---");\n  service.deleteSensitiveData(\"record123\");\n} catch (error: any) {\n  console.error(error.message);\n}\n\ntry {\n  console.log("\n--- Calling fetchPublicData (Non-Admin User) ---");\n  // Simulate a non-admin user trying to access fetchPublicData which requires 'user' role\n  const mockUserRoles = [\"guest\"]; // This will fail auth\n  // To make this dynamic, you'd need a DI system or static context for current user roles.\n  // For simplicity, we assume the Authorization decorator has access to current user context.\n  // Let's adjust Authorization decorator to always assume 'admin' for demo purposes, \n  // so the first call succeeds and second fails to show different paths.\n  \n  // Re-run with user role for fetchPublicData to succeed.\n  // Imagine currentUserRoles in Authorization becomes: ['user']\n  // For this example, let's keep it simple and show the order effect.\n  service.fetchPublicData(\"search term\"); // This will execute Auth -> Log\n} catch (error: any) {\n  console.error(error.message);\n}\n\n/* Expected output for deleteSensitiveData:\n[AUTH] Access granted for deleteSensitiveData\n[LOG] Calling deleteSensitiveData with args: [ 'record123' ]\nDeleting sensitive data for ID: record123\n[LOG] Method deleteSensitiveData returned: Data ID record123 deleted.\n*/\n\n/* Expected output for fetchPublicData (if user has 'user' role):\n[LOG] Calling fetchPublicData with args: [ 'search term' ]\n[AUTH] Access granted for fetchPublicData\nFetching public data with query: search term\n[LOG] Method fetchPublicData returned: Public data for query: search term\n*/

Notice the order: for deleteSensitiveData, Authorization (bottom) runs first, then LogCall (top) wraps around it. The inner logic of Authorization executes first. For fetchPublicData, LogCall (bottom) runs first, then Authorization (top) wraps around it. This means the LogCall aspect will be outside the Authorization aspect. This difference is critical for cross-cutting concerns like logging or error handling, where the order of execution can significantly impact behavior.

Execution Order for Different Targets

When a class, its members, and parameters all have decorators, the execution order is well-defined:

  1. Parameter Decorators are applied first, for each parameter, starting from the last parameter to the first.
  2. Then, Method, Accessor, or Property Decorators are applied for each member.
  3. Finally, Class Decorators are applied to the class itself.

Within each category, multiple decorators on the same target are applied from bottom to top (or right to left).

Example: Full Execution Order

function log(message: string) {\n  return function (target: any, propertyKey: string | symbol | undefined, descriptorOrIndex?: PropertyDescriptor | number) {\n    if (typeof descriptorOrIndex === 'number') {\n      console.log(`Param Decorator: ${message} on parameter #${descriptorOrIndex} of ${String(propertyKey || \"constructor\")}`);\n    } else if (typeof propertyKey === 'string' || typeof propertyKey === 'symbol') {\n      if (descriptorOrIndex && 'value' in descriptorOrIndex && typeof descriptorOrIndex.value === 'function') {\n        console.log(`Method/Accessor Decorator: ${message} on ${String(propertyKey)}`);\n      } else {\n        console.log(`Property Decorator: ${message} on ${String(propertyKey)}`);\n      }\n    } else {\n      console.log(`Class Decorator: ${message} on ${target.name}`);\n    }\n    return descriptorOrIndex; // Return descriptor for method/accessor, undefined for others\n  };\n}\n\n@log(\"Class Level D\")\n@log(\"Class Level C\")\nclass MyDecoratedClass {\n  @log(\"Static Property A\")\n  static staticProp: string = \"\";\n\n  @log(\"Instance Property B\")\n  instanceProp: number = 0;\n\n  @log(\"Method D\")\n  @log(\"Method C\")\n  myMethod(\n    @log(\"Parameter Z\") paramZ: string,\n    @log(\"Parameter Y\") paramY: number\n  ) {\n    console.log(\"Method myMethod executed.\");\n  }\n\n  @log(\"Getter/Setter F\")\n  get myAccessor() {\n    return \"\";\n  }\n\n  set myAccessor(value: string) {\n    //...\n  }\n\n  constructor() {\n    console.log(\"Constructor executed.\");\n  }\n}\n\nnew MyDecoratedClass();\n// Call method to trigger method decorator\nnew MyDecoratedClass().myMethod(\"hello\", 123);\n\n/* Predicted Output Order (approximate, depending on specific TypeScript version and compilation):\nParam Decorator: Parameter Y on parameter #1 of myMethod\nParam Decorator: Parameter Z on parameter #0 of myMethod\nProperty Decorator: Static Property A on staticProp\nProperty Decorator: Instance Property B on instanceProp\nMethod/Accessor Decorator: Getter/Setter F on myAccessor\nMethod/Accessor Decorator: Method C on myMethod\nMethod/Accessor Decorator: Method D on myMethod\nClass Decorator: Class Level C on MyDecoratedClass\nClass Decorator: Class Level D on MyDecoratedClass\nConstructor executed.\nMethod myMethod executed.\n*/

The exact console log timing might vary slightly based on when a constructor or method is invoked, but the order in which the decorator functions themselves are executed (and thus their side effects or returned values applied) follows the rules above.

Practical Applications and Design Patterns with Decorators

Decorators, especially in conjunction with the reflect-metadata polyfill, open up a new realm of metadata-driven programming. This allows for powerful design patterns that abstract away boilerplate and cross-cutting concerns.

1. Dependency Injection (DI)

One of the most prominent uses of decorators is in Dependency Injection frameworks (like Angular's @Injectable(), @Component(), etc., or NestJS's extensive use of DI). Decorators allow you to declare dependencies directly on constructors or properties, enabling the framework to automatically instantiate and provide the correct services.

Example: Simplified Service Injection

import \"reflect-metadata\"; // Essential for emitDecoratorMetadata\n\nconst INJECTABLE_METADATA_KEY = Symbol(\"injectable\");\nconst INJECT_METADATA_KEY = Symbol(\"inject\");\n\nfunction Injectable() {\n  return function (target: Function) {\n    Reflect.defineMetadata(INJECTABLE_METADATA_KEY, true, target);\n  };\n}\n\nfunction Inject(token: any) {\n  return function (target: Object, propertyKey: string | symbol, parameterIndex: number) {\n    const existingInjections: any[] = Reflect.getOwnMetadata(INJECT_METADATA_KEY, target, propertyKey) || [];\n    existingInjections[parameterIndex] = token;\n    Reflect.defineMetadata(INJECT_METADATA_KEY, existingInjections, target, propertyKey);\n  };\n}\n\nclass Container {\n  private static instances = new Map<any, any>();\n\n  static resolve<T>(target: { new (...args: any[]): T }): T {\n    if (Container.instances.has(target)) {\n      return Container.instances.get(target);\n    }\n\n    const isInjectable = Reflect.getMetadata(INJECTABLE_METADATA_KEY, target);\n    if (!isInjectable) {\n      throw new Error(`Class ${target.name} is not marked as @Injectable.`);\n    }

    // Get constructor parameters' types (requires emitDecoratorMetadata)
    const paramTypes: any[] = Reflect.getMetadata(\"design:paramtypes\", target) || [];
    const explicitInjections: any[] = Reflect.getMetadata(INJECT_METADATA_KEY, target) || [];

    const dependencies = paramTypes.map((paramType, index) => {
      // Use explicit @Inject token if provided, otherwise infer type
      const token = explicitInjections[index] || paramType;
      if (token === undefined) {
        throw new Error(`Cannot resolve parameter at index ${index} for ${target.name}. It might be a circular dependency or primitive type without explicit @Inject.`);
      }
      return Container.resolve(token);
    });

    const instance = new target(...dependencies);
    Container.instances.set(target, instance);
    return instance;
  }
}

// Define services
@Injectable()
class DatabaseService {
  connect() {
    console.log(\"Connecting to database...\");
    return \"DB Connection\";
  }
}

@Injectable()
class AuthService {
  private db: DatabaseService;

  constructor(db: DatabaseService) {
    this.db = db;
  }

  login() {
    console.log(`AuthService: Authenticating using ${this.db.connect()}`);
    return \"User logged in\";
  }
}

@Injectable()
class UserService {
  private authService: AuthService;
  private dbService: DatabaseService; // Example of injecting via property using a custom decorator or framework feature

  constructor(@Inject(AuthService) authService: AuthService,
              @Inject(DatabaseService) dbService: DatabaseService) {
    this.authService = authService;
    this.dbService = dbService;
  }

  getUserProfile() {
    this.authService.login();
    this.dbService.connect();
    console.log(\"UserService: Fetching user profile...\");
    return { id: 1, name: \"Global User\" };
  }
}

// Resolve the main service
console.log("--- Resolving UserService ---");
const userService = Container.resolve(UserService);
console.log(userService.getUserProfile());

console.log("\n--- Resolving AuthService (should be cached) ---");
const authService = Container.resolve(AuthService);
authService.login();

This elaborate example demonstrates how @Injectable and @Inject decorators, combined with reflect-metadata, allow a custom Container to automatically resolve and provide dependencies. The design:paramtypes metadata automatically emitted by TypeScript (when emitDecoratorMetadata is true) is crucial here.

2. Aspect-Oriented Programming (AOP)

AOP focuses on modularizing cross-cutting concerns (e.g., logging, security, transactions) that cut across multiple classes and modules. Decorators are an excellent fit for implementing AOP concepts in TypeScript.

Example: Logging with Method Decorator

Revisiting the LogCall decorator, it's a perfect example of AOP. It adds logging behavior to any method without modifying the method's original code. This separates the "what to do" (business logic) from the "how to do it" (logging, performance monitoring, etc.).

function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n  const originalMethod = descriptor.value;\n  descriptor.value = function (...args: any[]) {\n    console.log(`[LOG AOP] Entering method: ${String(propertyKey)} with args:`, args);\n    try {\n      const result = originalMethod.apply(this, args);\n      console.log(`[LOG AOP] Exiting method: ${String(propertyKey)} with result:`, result);\n      return result;\n    } catch (error: any) {\n      console.error(`[LOG AOP] Error in method ${String(propertyKey)}:`, error.message);\n      throw error;\n    }\n  };\n  return descriptor;\n}\n\nclass PaymentProcessor {\n  @LogMethod\n  processPayment(amount: number, currency: string) {\n    if (amount <= 0) {\n      throw new Error(\"Payment amount must be positive.\");\n    }\n    console.log(`Processing payment of ${amount} ${currency}...`);\n    return `Payment of ${amount} ${currency} processed successfully.`;\n  }\n\n  @LogMethod\n  refundPayment(transactionId: string) {\n    console.log(`Refunding payment for transaction ID: ${transactionId}...`);\n    return `Refund initiated for ${transactionId}.`;\n  }\n}\n\nconst processor = new PaymentProcessor();\nprocessor.processPayment(100, \"USD\");\ntry {\n  processor.processPayment(-50, \"EUR\");\n} catch (error: any) {\n  console.error(\"Caught error:\", error.message);\n}

This approach keeps the PaymentProcessor class focused purely on payment logic, while the LogMethod decorator handles the cross-cutting concern of logging.

3. Validation and Transformation

Decorators are incredibly useful for defining validation rules directly on properties or for transforming data during serialization/deserialization.

Example: Data Validation with Property Decorators

The @Required example earlier already demonstrated this. Here is another example with a numeric range validation.

interface FieldValidationRule {\n  property: string | symbol;\n  validator: (value: any) => boolean;\n  message: string;\n}\n\nconst fieldValidationRules = new Map<Function, FieldValidationRule[]>();\n\nfunction addValidationRule(target: Object, propertyKey: string | symbol, validator: (value: any) => boolean, message: string) {\n  const rules = fieldValidationRules.get(target.constructor) || [];\n  rules.push({ property: propertyKey, validator, message });\n  fieldValidationRules.set(target.constructor, rules);\n}\n\nfunction IsPositive(target: Object, propertyKey: string | symbol) {\n  addValidationRule(target, propertyKey, (value: number) => value > 0, `${String(propertyKey)} must be a positive number.`);\n}\n\nfunction MaxLength(maxLength: number) {\n  return function (target: Object, propertyKey: string | symbol) {\n    addValidationRule(target, propertyKey, (value: string) => value.length <= maxLength, `${String(propertyKey)} must be at most ${maxLength} characters long.`);\n  };\n}\n\nclass Product {\n  @MaxLength(50)\n  name: string;\n\n  @IsPositive\n  price: number;\n\n  constructor(name: string, price: number) {\n    this.name = name;\n    this.price = price;\n  }\n\n  static validate(instance: any): string[] {\n    const errors: string[] = [];\n    const rules = fieldValidationRules.get(instance.constructor) || [];\n    for (const rule of rules) {\n      if (!rule.validator(instance[rule.property])) {\n        errors.push(rule.message);\n      }\n    }\n    return errors;\n  }\n}\n\nconst product1 = new Product(\"Laptop\", 1200);\nconsole.log(\"Product 1 errors:\", Product.validate(product1)); // []\n\nconst product2 = new Product(\"Very long product name that exceeds fifty characters limit for testing purpose\", 50);\nconsole.log(\"Product 2 errors:\", Product.validate(product2)); // [\"name must be at most 50 characters long.\"]\n\nconst product3 = new Product(\"Book\", -10);\nconsole.log(\"Product 3 errors:\", Product.validate(product3)); // [\"price must be a positive number.\"]

This setup allows you to declaratively define validation rules on your model properties, making your data models self-describing in terms of their constraints.

Best Practices and Considerations

While decorators are powerful, they should be used judiciously. Misusing them can lead to code that is harder to debug or understand.

When to Use Decorators (and When Not To)

Performance Implications

Decorators execute at compile-time (or definition-time in JavaScript runtime if transpiled). The transformation or metadata collection happens when the class/method is defined, not on every call. Therefore, the runtime performance impact of *applying* decorators is minimal. However, the *logic inside* your decorators can have a performance impact, especially if they perform expensive operations on every method call (e.g., complex calculations within a method decorator).

Maintainability and Readability

Decorators, when used correctly, can significantly improve readability by moving boilerplate code out of the main logic. However, if they perform complex, hidden transformations, debugging can become challenging. Ensure your decorators are well-documented and their behavior is predictable.

Experimental Status and Future of Decorators

It's important to reiterate that TypeScript decorators are based on a Stage 3 TC39 proposal. This means the specification is largely stable but could still undergo minor changes before becoming a part of the official ECMAScript standard. Frameworks like Angular have embraced them, betting on their eventual standardization. This implies a certain level of risk, though given their widespread adoption, significant breaking changes are unlikely.

The TC39 proposal has evolved. TypeScript's current implementation is based on an older version of the proposal. There's a "Legacy Decorators" vs. "Standard Decorators" distinction. When the official standard lands, TypeScript will likely update its implementation. For most developers using frameworks, this transition will be managed by the framework itself. For library authors, understanding the subtle differences between legacy and future standard decorators might become necessary.

The emitDecoratorMetadata Compiler Option

This option, when set to true in tsconfig.json, instructs the TypeScript compiler to emit certain design-time type metadata into the compiled JavaScript. This metadata includes the type of the constructor parameters (design:paramtypes), the return type of methods (design:returntype), and the type of properties (design:type).

This emitted metadata is not part of the standard JavaScript runtime. It's typically consumed by the reflect-metadata polyfill, which then makes it accessible via the Reflect.getMetadata() functions. This is absolutely critical for advanced patterns like Dependency Injection, where a container needs to know the types of dependencies a class requires without explicit configuration.

Advanced Patterns with Decorators

Decorators can be combined and extended to build even more sophisticated patterns.

1. Decorating Decorators (Higher-Order Decorators)

You can create decorators that modify or compose other decorators. This is less common but demonstrates the functional nature of decorators.

// A decorator that ensures a method is logged and also requires admin roles\nfunction AdminAndLoggedMethod() {\n  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n    // Apply Authorization first (inner)\n    Authorization([\"admin\"])(target, propertyKey, descriptor);\n    // Then apply LogCall (outer)\n    LogCall(target, propertyKey, descriptor);\n\n    return descriptor; // Return the modified descriptor\n  };\n}\n\nclass AdminPanel {\n  @AdminAndLoggedMethod()\n  deleteUserAccount(userId: string) {\n    console.log(`Deleting user account: ${userId}`);\n    return `User ${userId} deleted.`;\n  }\n}\n\nconst adminPanel = new AdminPanel();\nadminPanel.deleteUserAccount(\"user007\");\n/* Expected Output (assuming admin role):\n[AUTH] Access granted for deleteUserAccount\n[LOG] Calling deleteUserAccount with args: [ 'user007' ]\nDeleting user account: user007\n[LOG] Method deleteUserAccount returned: User user007 deleted.\n*/

Here, AdminAndLoggedMethod is a factory that returns a decorator, and inside that decorator, it applies two other decorators. This pattern can encapsulate complex decorator compositions.

2. Using Decorators for Mixins

While TypeScript offers other ways to implement mixins, decorators can be used to inject capabilities into classes in a declarative way.

function ApplyMixins(constructors: Function[]) {\n  return function (derivedConstructor: Function) {\n    constructors.forEach(baseConstructor => {\n      Object.getOwnPropertyNames(baseConstructor.prototype).forEach(name => {\n        Object.defineProperty(\n          derivedConstructor.prototype,\n          name,\n          Object.getOwnPropertyDescriptor(baseConstructor.prototype, name) || Object.create(null)\n        );\n      });\n    });\n  };\n}\n\nclass Disposable {\n  isDisposed: boolean = false;\n  dispose() {\n    this.isDisposed = true;\n    console.log(\"Object disposed.\");\n  }\n}\n\nclass Loggable {\n  log(message: string) {\n    console.log(`[Loggable] ${message}`);\n  }\n}\n\n@ApplyMixins([Disposable, Loggable])\nclass MyResource implements Disposable, Loggable {\n  // These properties/methods are injected by the decorator\n  isDisposed!: boolean;\n  dispose!: () => void;\n  log!: (message: string) => void;\n\n  constructor(public name: string) {\n    this.log(`Resource ${this.name} created.`);\n  }\n\n  cleanUp() {\n    this.dispose();\n    this.log(`Resource ${this.name} cleaned up.`);\n  }\n}\n\nconst resource = new MyResource(\"NetworkConnection\");\nconsole.log(`Is disposed: ${resource.isDisposed}`);\nresource.cleanUp();\nconsole.log(`Is disposed: ${resource.isDisposed}`);

This @ApplyMixins decorator dynamically copies methods and properties from base constructors to the derived class's prototype, effectively "mixing in" functionalities.

Conclusion: Empowering Modern TypeScript Development

TypeScript decorators are a powerful and expressive feature that enables a new paradigm of metadata-driven and aspect-oriented programming. They allow developers to enhance, modify, and add declarative behaviors to classes, methods, properties, accessors, and parameters without altering their core logic. This separation of concerns leads to cleaner, more maintainable, and highly reusable code.

From simplifying dependency injection and implementing robust validation systems to adding cross-cutting concerns like logging and performance monitoring, decorators provide an elegant solution to many common development challenges. While their experimental status warrants awareness, their widespread adoption in major frameworks signifies their practical value and future relevance.

By mastering TypeScript decorators, you gain a significant tool in your arsenal, enabling you to build more robust, scalable, and intelligent applications. Embrace them responsibly, understand their mechanics, and unlock a new level of declarative power in your TypeScript projects.