Introduction
In TypeScript, decorators have long been a key feature, popularized by major frameworks like Angular, NestJS, and TypeORM to manage metadata and dependency injection.
However, for years, decorators remained an experimental feature (requiring the experimentalDecorators flag), relying on a legacy proposal that differed from the evolving ECMAScript (JavaScript) standard.
With the release of TypeScript 5.0, official support for the standard ECMAScript Decorators (Stage 3/4) was introduced. This brings improved type safety and runtime consistency to the decorator syntax. This article explains how to write standard decorators in modern TypeScript.
1. Experimental vs. Standard Decorators
The primary difference lies in the configuration requirements and runtime API signature of the decorator functions.
Configuration Changes
- Legacy (Experimental):
Required
"experimentalDecorators": trueinside yourtsconfig.json. - Standard (New): Runs out-of-the-box by default. If compile targets support the latest ECMAScript features, standard decorators transpile natively.
Context Objects and Type Safety
Legacy decorators had access to loose property descriptors, making type safety difficult to enforce. Standard decorators receive a structured context object as their second argument, providing type-safe metadata about the decorated target (such as its name, visibility, and target kind).
2. Writing a Standard Method Decorator
Let’s write a standard logging decorator that prints messages to the console whenever a class method is executed.
Decorator Definition
// Standard method decorator signature
function logMethod<This, Args extends any[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
const methodName = String(context.name);
// Return a wrapped replacement function
return function (this: This, ...args: Args): Return {
console.log(`[LOG] Calling ${methodName} with arguments:`, args);
const result = target.apply(this, args);
console.log(`[LOG] Finished ${methodName}. Returned:`, result);
return result;
};
}
Applying the Decorator
class UserService {
@logMethod
getUser(id: number) {
return { id, name: "Takao" };
}
}
const service = new UserService();
service.getUser(42); // Prints log messages during execution
ClassMethodDecoratorContext: Provides access to target metadata, such as whether the method is static, private, or what name it holds (context.name), ensuring type safety at compile time.
3. Key Migration Caveats
While standard decorators improve type safety, migrating legacy codebases requires caution:
- Reflect Metadata Interoperability:
Legacy decorators often relied on the
reflect-metadatalibrary to runtime-inject dependencies (e.g., in NestJS). The standard decorator proposal implements a different metadata model. Verify framework compatibility before disabling experimental flags. - Deprecation of Parameter Decorators:
Decorators applied directly to method arguments (e.g.,
updateUser(@Inject() id: number)) were excluded from the final ECMAScript Stage 3 standard. Standard decorators can only target classes, methods, fields, and accessors.
Conclusion
The standardization of decorators in TypeScript 5.0 aligns the language with the future of JavaScript.
- Use decorators natively without configuring the
experimentalDecoratorsflag. - Leverage the
contextparameter to query target metadata safely. - Be mindful of parameter decorator deprecations when refactoring legacy codebases.
Adopting standard decorators helps ensure your metadata-driven TypeScript code remains compatible with the evolving JavaScript standard.
