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.
| Argument | Type | Meaning |
|---|---|---|
target | object | Prototype (instance) or constructor (static) |
propertyKey | string | symbol | undefined | Method name; undefined for constructors |
parameterIndex | number | 0-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
experimentalDecoratorsandemitDecoratorMetadatafor DI to work. - Store collected indices in metadata keyed by the method, then act on them in a method or class decorator.
- Handle
propertyKey === undefinedto support constructor parameters, not just method parameters. - Use
@Inject(TOKEN)to inject interfaces, since erased interface types cannot appear indesign:paramtypes. - Keep parameter decorators side-effect-light; their job is to register, not to execute logic.