Now, let’s talk about the black sheep of the decorator family: parameter decorators. You’ve probably seen them lurking in Angular or other DI-heavy frameworks, looking cryptic and a bit magical. Their entire job is to let you attach metadata to a function’s parameters. That’s it. They don’t do anything on their own. They just whisper, “Hey, when you’re constructing this class later, you should probably pass 'database' for this first parameter, okay?” They are the ultimate backseat drivers of the JavaScript world.

Here’s the kicker: as of ES2023, they are still firmly in the “Experimental” stage. The TC39 committee is clearly still trying to figure out what to do with them. The spec warns that they might change at any time. So, if you’re using them in a framework like Angular or NestJS, you’re riding on a framework-specific implementation. It works, but it’s not yet a language standard you can rely on everywhere.

The Anatomy of a Parameter Decorator

The signature is a bit weird, mostly because this thing is called in a very specific context. Here’s what it looks like:

function MyParameterDecorator(
  target: Object,           // The class prototype (for instance methods) or the constructor function itself (for static methods)
  propertyKey: string | symbol, // The name of the method the parameter belongs to (undefined for constructor)
  parameterIndex: number    // The index of the parameter in the function's parameter list
) {
  // Your job: attach some metadata somewhere
}

Let’s break this down with a concrete, if simplistic, example. Imagine we’re building a bare-bones dependency injection system. We need to mark parameters that should be injected.

// A simple Map to store our metadata. In a real framework, this would be far more robust.
const injectionTokens = new Map();

// This is our parameter decorator. We'll call it `Inject`.
function Inject(token: any) {
  return (target: Object, propertyKey: string | symbol, parameterIndex: number) => {
    // We need a unique key to store the metadata for this specific class constructor or method.
    // For a constructor parameter, `propertyKey` is undefined, so we use 'constructor'.
    const injectionKey = propertyKey || 'constructor';
    const className = target.constructor.name;

    console.log(`Decorating parameter ${parameterIndex} of ${String(propertyKey)} in class ${className}`);

    // Get the existing metadata for this class/method, or create a new array.
    const existingMetadata = injectionTokens.get(className) || {};
    const existingParams = existingMetadata[injectionKey] || [];

    // Store the token for this specific parameter index.
    existingParams[parameterIndex] = token;

    // Update the metadata store.
    existingMetadata[injectionKey] = existingParams;
    injectionTokens.set(className, existingMetadata);
  };
}

// A couple of "services" to inject
class DatabaseService {
  getData() { return 'Data from the database!'; }
}
class LoggerService {
  log(message: string) { console.log(`LOG: ${message}`); }
}

class MyRepository {
  // Use the decorator on the constructor parameters.
  // This marks the first param for a DatabaseService and the second for a LoggerService.
  constructor(
    @Inject(DatabaseService) private db: any,
    @Inject(LoggerService) private logger: any
  ) {}

  getData() {
    const data = this.db.getData();
    this.logger.log('Fetched data');
    return data;
  }
}

// A very naive "Injector" that reads our metadata and creates instances.
function createInstance<T>(Clazz: new (...args: any[]) => T): T {
  const className = Clazz.name;
  const metadata = injectionTokens.get(className);
  const constructorParams = metadata?.['constructor'] || [];

  // Resolve each dependency. A real injector would have a container to get instances.
  const resolvedDependencies = constructorParams.map((Token: any) => new Token());

  // Instantiate the class with the resolved dependencies.
  return new Clazz(...resolvedDependencies);
}

// Let's see it in action!
const myInstance = createInstance(MyRepository);
console.log(myInstance.getData()); // Output: "LOG: Fetched data" & "Data from the database!"

Why It Feels So Janky

You’ll notice the decorator doesn’t inject anything. It just leaves little breadcrumbs (injectionTokens) for some other part of your system (the createInstance function in our example) to find and act upon later. This indirection is why it feels so abstract. You’re not writing the logic that does the thing; you’re writing the logic that records what should be done.

The most common pitfall here is getting lost in the metadata. You’re dealing with class names, method names, and index numbers. It’s stringly-typed programming at its finest, and it’s incredibly easy to make a mistake that only shows up at runtime. This is a big part of why the language designers are hesitant to standardize it—it’s a powerful but sharp tool.

Best Practices and the Future

  1. Use a Library: For the love of all that is holy, do not roll your own production DI system based on this experimental feature. Use a framework that abstracts this complexity away (like Angular, NestJS, or InversifyJS). They handle the metadata storage and reflection in a robust, tested way.
  2. Awareness of undefined: Remember, for constructor parameters, propertyKey is undefined. This is a classic gotcha. Always have a fallback (like 'constructor') when building your metadata key.
  3. The Standardization Wait: Keep an eye on the TC39 proposals. The future of parameter decorators is likely tied to the broader “Decorator Metadata” proposal, which would provide a standard way (context.metadata) to attach this information, making it less of a hack.

So, while parameter decorators are undeniably cool and enable the magic of modern DI frameworks, treat them with respect. They are the duct tape and baling wire of the decorator world—incredibly useful for holding a complex system together, but you probably shouldn’t build the whole system out of them yourself. At least, not until the language gives us a proper standard.