Skip to content
TypeScript ts decorators 3 min read

Parameter Decorators

A parameter decorator attaches to a single parameter of a constructor or method, recording which slot it occupies so a framework can later supply or validate that argument. The single most important fact about parameter decorators is that they exist only in the legacy experimentalDecorators system — the Stage 3 (TypeScript 5.0) standard has no parameter decorators at all. They have no return value and cannot change the parameter; they only register metadata. This is the mechanism behind NestJS’s @Inject, @Param, @Body, and Angular’s constructor injection. This page explains the signature, the index argument, and the dependency-injection pattern they enable.

Legacy-only: no Stage 3 equivalent

If you enable Stage 3 decorators (the default in TS 5.0 without experimentalDecorators), placing a decorator on a parameter is a compile error. Parameter decorators require the legacy flag.

// tsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}
// ❌ Error under Stage 3: "Decorators are not valid here."
class Stage3 {
  greet(@upper name: string) {} // not allowed without experimentalDecorators
}

The TC39 proposal deliberately left parameter decorators out of the first standard release, so for injection-style frameworks you must stay on the legacy system until a future proposal lands.

The parameter decorator signature

A legacy parameter decorator is a function (target, propertyKey, parameterIndex). It returns nothing. The parameterIndex tells you which argument position was decorated (0-based).

// tsconfig: "experimentalDecorators": true
function logParam(target: object, propertyKey: string | symbol, index: number) {
  console.log(`Decorated param #${index} of ${String(propertyKey)}`);
}

class Greeter {
  greet(prefix: string, @logParam name: string) {
    return `${prefix} ${name}`;
  }
}
// logs: Decorated param #1 of greet

For constructor parameters, propertyKey is undefined because the constructor has no property name. The target is the class prototype for instance methods, or the constructor function for static methods.

ArgumentTypeMeaning
targetobjectPrototype (instance) or constructor (static)
propertyKeystring | symbol | undefinedMethod name; undefined for constructors
parameterIndexnumber0-based position of the parameter

Recording metadata for injection

Because a parameter decorator only knows which index it sits on, the usual pattern is to store that index in metadata so a runtime can read it back. Here we mark required parameters and validate them in a method decorator.

import "reflect-metadata";

const REQUIRED = Symbol("required");

function Required(target: object, key: string | symbol, index: number) {
  const existing: number[] = Reflect.getOwnMetadata(REQUIRED, target, key) ?? [];
  existing.push(index);
  Reflect.defineMetadata(REQUIRED, existing, target, key);
}

Each time @Required is applied, it appends the parameter’s index to an array stored on the method. A companion method decorator can later loop over those indices and throw if any matching argument is missing.

The dependency-injection pattern

The real power appears when combined with emitDecoratorMetadata. With both flags on, the compiler emits a design:paramtypes array — the constructor’s parameter types — which a DI container reads to know what to instantiate.

import "reflect-metadata";

function Injectable<T extends new (...args: any[]) => any>(ctor: T) {
  return ctor;
}

@Injectable
class Logger {}

@Injectable
class UserService {
  constructor(private logger: Logger) {}
}

// A container reads the emitted parameter types:
const deps = Reflect.getMetadata("design:paramtypes", UserService);
// deps === [Logger]  → container knows to inject a Logger instance

The container resolves design:paramtypes to [Logger], constructs a Logger, and passes it in. NestJS layers @Inject("TOKEN") on top to override the inferred type for interface-based providers. See Metadata & Reflection for how design:paramtypes is generated.

Best Practices

  • Remember parameter decorators are legacy-only; do not expect them under Stage 3.
  • Always import "reflect-metadata" once at your app’s entry point before any decorated class loads.
  • Enable both experimentalDecorators and emitDecoratorMetadata for DI to work.
  • Store collected indices in metadata keyed by the method, then act on them in a method or class decorator.
  • Handle propertyKey === undefined to support constructor parameters, not just method parameters.
  • Use @Inject(TOKEN) to inject interfaces, since erased interface types cannot appear in design:paramtypes.
  • Keep parameter decorators side-effect-light; their job is to register, not to execute logic.
Last updated June 29, 2026
Was this helpful?