TypeScript Parameter Decorators

In TypeScript, parameter decorators are a specialized type of decorator used to observe or add metadata to individual function parameters. While they are less common than class or method decorators, they are incredibly powerful when building frameworks or libraries that require deep introspection of your code, such as those used for logging, validation, or dependency injection.

Developer Tip: Parameter decorators are most effective when paired with "Reflect Metadata." This allows you to store data about a parameter during the design phase and read it back during runtime.

 

What is a Parameter Decorator?

A parameter decorator is a function declared just before a parameter declaration. It is applied to the parameters of a class method or a constructor. Unlike method decorators, a parameter decorator cannot directly change the value of the argument being passed; instead, it provides information about that parameter that can be used later by other decorators or the class itself.

Syntax of Parameter Decorators

The decorator function for parameters accepts three specific arguments:

function ParameterDecorator(target: any, methodName: string | symbol, parameterIndex: number) {
  // Logic to register metadata or observe the parameter
}
  • target: Either the constructor function of the class (for a static member) or the prototype of the class (for an instance member).
  • methodName: The name of the method the parameter belongs to. For a constructor, this is undefined.
  • parameterIndex: The ordinal index of the parameter in the function’s argument list (starting at 0).
Best Practice: Use parameter decorators primarily for "tagging" parameters. If you need to modify the actual value or behavior of the method call, combine the parameter decorator with a Method Decorator.

 

Enabling Parameter Decorators in TypeScript

By default, TypeScript does not enable decorator support. You must explicitly allow them in your configuration. Open your tsconfig.json file and ensure the following flag is set to true:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}
Watch Out: Decorators are currently an "experimental" feature in TypeScript. While they are widely used in frameworks like Angular and NestJS, their syntax may evolve in future versions of JavaScript/TypeScript.

 

Applying a Simple Parameter Decorator

A simple parameter decorator can log details about how a class is structured. This is often used during development to debug method signatures or ensure that metadata is being correctly applied.

Example: Applying a Simple Parameter Decorator

function LogParameter(target: any, methodName: string, parameterIndex: number) {
  console.log(`Analyzing: Method "${methodName}" has a parameter at index ${parameterIndex}`);
}

class UserSettings {
  updateEmail(@LogParameter newEmail: string) {
    console.log(`Email updated to: ${newEmail}`);
  }
}

// Output when the code is loaded (not when called):
// Analyzing: Method "updateEmail" has a parameter at index 0
  • The @LogParameter decorator runs as soon as the class is defined by the JavaScript engine, not when the updateEmail method is actually executed.
Common Mistake: Thinking that a parameter decorator runs every time the function is called. In reality, it runs only once when the class is first defined.

 

Using Parameter Decorators for Validation

Validation is a common real-world use case. Because a parameter decorator cannot stop a function from running, we usually "mark" a parameter as required and then use a separate validator to check it.

Example: Parameter Decorator for Validation

import "reflect-metadata";

const REQUIRED_METADATA_KEY = "requiredParameters";

function Required(target: any, methodName: string, parameterIndex: number) {
  let existingRequiredParameters: number[] = Reflect.getOwnMetadata(REQUIRED_METADATA_KEY, target, methodName) || [];
  existingRequiredParameters.push(parameterIndex);
  Reflect.defineMetadata(REQUIRED_METADATA_KEY, existingRequiredParameters, target, methodName);
}

function validate(target: any, methodName: string, args: any[]) {
  let requiredParameters: number[] = Reflect.getOwnMetadata(REQUIRED_METADATA_KEY, target, methodName);
  if (requiredParameters) {
    for (let parameterIndex of requiredParameters) {
      if (parameterIndex >= args.length || args[parameterIndex] === undefined) {
        throw new Error(`Missing required argument at position ${parameterIndex} in ${methodName}`);
      }
    }
  }
}

class BugTracker {
  saveBug(@Required title: string) {
    console.log(`Bug saved: ${title}`);
  }
}

const tracker = new BugTracker();
// Manually calling validation (In frameworks, this is automated via method decorators)
validate(tracker, "saveBug", []); // Throws: Missing required argument at position 0 in saveBug

 

Parameter Decorators for Dependency Injection

If you have used frameworks like NestJS or Angular, you have seen parameter decorators used for Dependency Injection (DI). They tell the system which specific service should be "injected" into a class constructor.

Example: Dependency Injection with Parameter Decorators

function Inject(token: string) {
  return function(target: any, methodName: string, parameterIndex: number) {
    console.log(`Metadata: Parameter at index ${parameterIndex} needs service: ${token}`);
    // The DI container would use this info to provide the correct instance
  };
}

class DatabaseService {}

class AppController {
  constructor(@Inject('DB_SERVICE') private db: DatabaseService) {
    console.log('Controller initialized');
  }
}
  • The @Inject decorator acts as a "marker" that helps an external "Container" or "Injector" understand what the class needs to function.

 

Using Parameter Decorators with Reflect Metadata

The reflect-metadata library is the standard way to work with decorators in TypeScript. It provides a centralized registry for storing information about classes and their members.

Example: Using Reflect Metadata with Parameter Decorators

import "reflect-metadata";

function Role(roleName: string) {
  return function(target: any, methodName: string, parameterIndex: number) {
    const roles = Reflect.getOwnMetadata("roles", target, methodName) || {};
    roles[parameterIndex] = roleName;
    Reflect.defineMetadata("roles", roles, target, methodName);
  };
}

class AdminPanel {
  deleteUser(@Role("admin") userId: string) {
    console.log(`User ${userId} deleted.`);
  }
}

// Checking metadata later
const meta = Reflect.getOwnMetadata("roles", AdminPanel.prototype, "deleteUser");
console.log(meta); // Output: { '0': 'admin' }
Developer Tip: When using reflect-metadata, you must import it once at the entry point of your application (like index.ts or main.ts).

 

Parameter Decorators with Custom Logic

While parameter decorators don't have the power to change the method's behavior on their own, you can get creative by modifying the method's prototype inside the decorator. However, this is generally considered advanced and should be used with caution.

Example: Parameter Decorator with Custom Logic

function AutoUpper(target: any, methodName: string, parameterIndex: number) {
  const originalMethod = target[methodName];

  // Replacing the original method with a wrapper that modifies arguments
  target[methodName] = function(...args: any[]) {
    if (typeof args[parameterIndex] === "string") {
      args[parameterIndex] = args[parameterIndex].toUpperCase();
    }
    return originalMethod.apply(this, args);
  };
}

class Logger {
  log(@AutoUpper message: string) {
    console.log(`LOG: ${message}`);
  }
}

const myLogger = new Logger();
myLogger.log("check database connection"); // Output: LOG: CHECK DATABASE CONNECTION
Watch Out: Directly overwriting target[methodName] inside a parameter decorator can lead to issues if multiple parameter decorators are applied to the same method. A more robust way is to use a Method Decorator to handle argument manipulation.

 

Summary

  • Purpose: Parameter decorators are used to "tag" or add metadata to method arguments.
  • Execution: They run at class definition time, not at runtime when the method is called.
  • Parameters: They receive the target prototype, methodName, and parameterIndex.
  • Ecosystem: They are fundamental for building Dependency Injection and Validation systems in modern TypeScript frameworks.
  • Tooling: Use the reflect-metadata library to maximize the utility of these decorators.