TypeScript Decorators: Standard vs experimentalDecorators
Choose the TypeScript 5.0 decorator model or legacy experimentalDecorators by comparing signatures, metadata, parameters, and migration limits.
TypeScript supports two decorator systems with the same @name syntax but different function contracts and emit. Since TypeScript 5.0, decorators are accepted without experimentalDecorators and use the newer JavaScript proposal model. Enabling experimentalDecorators selects TypeScript’s older legacy model instead. Existing decorator functions are not automatically portable between them.
Choose the model before choosing a library or writing a decorator. The newer system provides context objects and initializers, but it does not support parameter decorators or emitDecoratorMetadata. Frameworks built around constructor-parameter metadata may therefore still require the legacy flags.
Standard decorators receive a value and context
A current method decorator receives the original method followed by a ClassMethodDecoratorContext. It can return a replacement with the same calling role:
function logged<This, Args extends unknown[], Return>(
original: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<
This,
(this: This, ...args: Args) => Return
>,
) {
const name = String(context.name);
return function (this: This, ...args: Args): Return {
console.log(`enter ${name}`);
const result = original.call(this, ...args);
console.log(`exit ${name}`);
return result;
};
}
class Greeter {
@logged
greet(name: string): string {
return `Hello, ${name}`;
}
}
console.log(new Greeter().greet('Ada'));
Compile this with TypeScript 5.0 or newer without experimentalDecorators. The output logs entry, exit, and Hello, Ada. The TypeScript 5.0 decorator documentation defines the value-and-context contract and the context types for classes, methods, fields, accessors, getters, and setters.
A decorator can also return nothing to leave the value unchanged. context.addInitializer schedules setup at a defined point in class or instance initialization, which is useful for registration and method binding without replacing the method itself.
Legacy decorators receive targets and property keys
With experimentalDecorators: true, a method decorator follows the older TypeScript contract:
function legacyLogged(
target: object,
propertyKey: string | symbol,
descriptor: PropertyDescriptor,
): void {
const original = descriptor.value as (...args: unknown[]) => unknown;
descriptor.value = function (this: object, ...args: unknown[]) {
console.log(`call ${String(propertyKey)}`);
return original.apply(this, args);
};
}
class LegacyGreeter {
@legacyLogged
greet(name: string): string {
return `Hello, ${name}`;
}
}
The decorator receives a prototype or constructor target, a property key, and a descriptor. That signature is not the value-and-context signature above. The legacy decorators handbook documents the older class, method, accessor, property, and parameter contracts and requires the opt-in flag.
Do not identify the model from syntax alone. Check tsconfig.json, the library’s installation instructions, and the decorator’s callable type.
Application order is observable
Decorator expressions are evaluated from top to bottom. Their returned decorator functions are applied in reverse order, so the decorator closest to a method receives the original first:
class Service {
@outer
@inner
run() {}
}
inner transforms run, then outer receives that result. Both systems apply multiple decorators in a composition-like reverse order, although their initialization details and ordering across different class elements are not interchangeable. Avoid relying on side effects during decorator evaluation when a returned replacement or initializer can express the dependency explicitly.
Metadata and parameters divide the systems
The standard model implemented in TypeScript 5.0 cannot decorate parameters and is incompatible with emitDecoratorMetadata. The official comparison with experimental legacy decorators calls out both limits.
Legacy projects commonly pair these options:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
emitDecoratorMetadata adds design-type metadata calls to emitted JavaScript and requires a compatible runtime metadata implementation for code that reads them. The emitted design types are limited runtime approximations, not the full TypeScript type system: interfaces and unions do not become runtime validators.
If a dependency-injection framework asks for both flags or uses constructor parameter decorators, disabling them is an architectural migration, not a compiler cleanup. Confirm that the framework supports the standard model before converting application decorators.
Migration means rewriting contracts
A safe migration starts with an inventory:
- Find parameter decorators and metadata consumers. They have no direct equivalent in the TypeScript 5.0 model.
- Classify each decorator by element kind and whether it replaces a value, mutates a descriptor, registers metadata, or performs initialization.
- Rewrite against the relevant context type and
addInitializerwhere needed. - Test evaluation, application, instance initialization, inheritance, static members, and private-member rejection.
- Remove
experimentalDecoratorsonly after every decorator producer and dependency uses the newer contract.
A compatibility wrapper is possible for simple method decorators, but one function must distinguish two unrelated call shapes at runtime and retain accurate overloads. Separate entry points are usually easier to type and test.
Decorator emit also requires an actual compilation step. Node.js type stripping does not transform decorators, so direct node file.ts execution is not a substitute for TypeScript’s decorator emit.
For a new codebase without a legacy framework constraint, use the standard decorator model and leave experimentalDecorators off. For an existing metadata-driven framework, keep the legacy configuration until the framework provides a documented migration path. The flag is a semantic choice between two systems, not a switch that merely enables the syntax.