Key Takeaways
- TypeScript decorators can wrap behavior or attach metadata, making them useful for logging, monitoring, caching, and validation.
- Standard decorators use a value/context API. Setting experimentalDecorators to true selects the older, legacy API.
- Legacy decorators support parameter decorators and optional emitted type metadata; standard decorators do not support either feature.
- Metadata describes something; a wrapper, framework, or other runtime component must consume it to change behavior.
TypeScript decorators are functions applied with @ syntax to classes and class members. They can replace a method with a wrapper, modify a declaration, or record metadata for other code to read.
The important compatibility boundary is which decorator system your project uses. TypeScript 5.0 introduced the proposal-based API commonly called standard decorators; the original examples below remain useful, but their signatures belong to legacy decorators. The TypeScript 5.0 release notes explain why the two APIs aren't interchangeable.
Standard vs. Legacy TypeScript Decorators
| Feature | Standard decorators | Legacy decorators |
|---|---|---|
| Compiler configuration | Leave experimentalDecorators unset or false | Set experimentalDecorators to true |
| Method arguments | method, context | target, propertyKey, descriptor |
| Supported declarations | Classes, methods, getters, setters, fields, and auto-accessors | Classes, methods, accessors, properties, and parameters |
| Parameter decorators | Not supported | Supported |
| Property descriptor access | No descriptor argument | Available for methods and accessors |
| Metadata | context.metadata and Symbol.metadata, with the appropriate runtime support | Explicit reflect-metadata usage; optional emitDecoratorMetadata |
Here, “standard” identifies the proposal-based API, not a promise that every runtime can execute decorator syntax directly. These examples compile TypeScript to JavaScript before execution. The decorators proposal documents the declaration kinds and replacement rules.
Set Up and Run the Examples
Use a supported Node.js installation with npm. In an empty directory, create this package.json:
{
"private": true,
"type": "module"
}Install the compiler:
npm install --save-dev typescriptCreate tsconfig.json. This starts in standard-decorator mode:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"lib": ["ES2022", "DOM"],
"strict": true,
"experimentalDecorators": false,
"emitDecoratorMetadata": false,
"outDir": "dist"
},
"files": ["demo.ts"]
}Save one complete example at a time as demo.ts, then compile and run it:
npx tsc --project tsconfig.json
node dist/demo.jsThe package's module setting and NodeNext configuration keep these examples in ESM format. Use --project rather than passing a source filename: passing input files directly makes the compiler ignore tsconfig.json. See the module reference, compiler CLI documentation, and our guide to TypeScript tsconfig.json.
Standard TypeScript Method Decorator Example
A standard method decorator receives the original method and a context object. Return a function to replace the method; context.name identifies the decorated member.
function log<This, Args extends unknown[], Result>(
method: (this: This, ...args: Args) => Result,
context: ClassMethodDecoratorContext<
This,
(this: This, ...args: Args) => Result
>,
) {
const name = String(context.name);
return function (this: This, ...args: Args): Result {
console.log(`before ${name}`);
try {
return method.apply(this, args);
} finally {
console.log(`after ${name}`);
}
};
}
class SomeClass {
@log
someMethod(): void {
console.log("running someMethod");
}
}
new SomeClass().someMethod();Without the decorator, the method prints only running someMethod. With it:
before someMethod
running someMethod
after someMethodThe wrapper forwards the receiver, arguments, and return value. Its finally block also runs when the original method throws synchronously, without swallowing that exception.
Legacy TypeScript Decorator Examples
For every example in this section, change experimentalDecorators to true. Keep emitDecoratorMetadata false: none of these examples needs automatically emitted type metadata. The legacy decorator handbook documents these signatures.
Class Decorator: Return a Replacement Subclass
function smithFamily<
T extends new (...args: any[]) => object
>(Base: T) {
return class extends Base {
lastName = "Smith";
};
}
@smithFamily
class Person {
firstName: string;
constructor(firstName: string) {
this.firstName = firstName;
}
}
console.log(JSON.stringify(new Person("John")));{"firstName":"John","lastName":"Smith"}This doesn't merely annotate Person: it replaces the constructor with a subclass. Extending the original class preserves its inheritance chain and forwards construction to the original constructor.
The decorator does not add lastName to the declared TypeScript instance type. Accessing new Person("John").lastName would still be a type error; declare that contract explicitly or use a typed factory when callers need the added member. Our introduction to JavaScript classes covers the underlying inheritance model.
Method Decorator: Add Logging Before Execution
function log<This, Args extends unknown[], Result>(
_target: object,
_propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<
(this: This, ...args: Args) => Result
>,
): void {
const originalMethod = descriptor.value;
if (originalMethod === undefined) {
throw new TypeError("Expected a method");
}
descriptor.value = function (this: This, ...args: Args): Result {
console.log("logging a statement before running function");
return originalMethod.apply(this, args);
};
}
class SomeClass {
@log
someMethod(): void {
console.log("running someMethod");
}
}
new SomeClass().someMethod();logging a statement before running function
running someMethodFor an instance method, target is the class prototype; for a static method, it's the constructor. The descriptor exposes the method as value, which this decorator replaces. Compare that three-argument API with the standard example's method/context pair.
Accessor Decorator: Change Enumerability
function canEnumerate(value: boolean) {
return function (
_target: object,
_propertyKey: string | symbol,
descriptor: PropertyDescriptor,
): void {
descriptor.enumerable = value;
};
}
class SomeClass {
_x: number;
_y: number;
constructor(x: number, y: number) {
this._x = x;
this._y = y;
}
@canEnumerate(true)
get x(): number {
return this._x;
}
@canEnumerate(false)
get y(): number {
return this._y;
}
}
const point = new SomeClass(1, 2);
for (const key in point) {
const value: unknown = Reflect.get(point, key);
console.log(`${key} = ${value}`);
}_x = 1
_y = 2
x = 1The backing fields _x and _y are enumerable own properties. The getters live on the prototype; the decorator makes x enumerable while leaving y out of enumeration.
for...in includes inherited enumerable string properties, which explains all three output lines. Using Reflect.get avoids indexing a class instance with an arbitrary string under strict checking. For the object model behind descriptors, see JavaScript objects.
This descriptor-changing technique is legacy-specific: a standard getter decorator receives the getter function, not its property descriptor.
Property Decorator: Observe a Declared Property
function printKey(_target: object, propertyKey: string | symbol): void {
console.log(String(propertyKey));
}
class SomeClass {
@printKey
someProp: string = "";
}
new SomeClass();somePropThe message prints when the class is defined, not whenever an instance is created or the property changes. A legacy property decorator receives no descriptor, cannot directly replace the field initializer, and has its return value ignored.
Parameter Decorator: Observe a Parameter Position
function printIndex(
_target: object,
_propertyKey: string | symbol | undefined,
index: number,
): void {
console.log(`Parameter index is: ${index}`);
}
class SomeClass {
someMethod(
first: string,
second: string,
@printIndex third: string,
): void {}
}
new SomeClass();Parameter index is: 2Parameter positions are zero-based. This decorator observes the declaration; it doesn't receive argument values or validate calls. The property key is undefined for constructor parameters. Standard decorators do not support parameter decorators.
TypeScript Decorators and Metadata
Legacy reflect-metadata Example
Install reflect-metadata, remain in legacy mode, and import the library before decorated declarations execute:
npm install reflect-metadataimport "reflect-metadata";
const formatMetadataKey = Symbol("format");
function format(template: string): PropertyDecorator {
return Reflect.metadata(formatMetadataKey, template);
}
function getFormat(target: object, propertyKey: string | symbol): string {
const value: unknown = Reflect.getMetadata(
formatMetadataKey, target, propertyKey,
);
if (typeof value !== "string") {
throw new TypeError("Missing format metadata");
}
return value;
}
class Greeter {
@format("Hello, %s")
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet(): void {
console.log(getFormat(this, "greeting").replace("%s", this.greeting));
}
}
new Greeter("Alex").greet();Hello, AlexThe metadata belongs to Greeter.prototype, associated with the property key greeting. Reflect.getMetadata searches the prototype chain, so looking it up through the instance finds that entry. The reflect-metadata documentation describes this storage and lookup model.
This explicit metadata needs no compiler-generated type information. Separately, setting emitDecoratorMetadata to true in legacy mode emits limited design metadata using keys such as design:type, design:paramtypes, and design:returntype. It isn't complete reflection: interfaces and generic type arguments aren't preserved as runtime type definitions. TypeScript's type annotations are erased during compilation.
Standard Decorator Metadata Uses a Different Mechanism
TypeScript 5.2 added support for context.metadata, a shared metadata object exposed afterward as Class[Symbol.metadata]. This is not the Reflect.metadata API and does not automatically serialize TypeScript types.
For this example, return to standard mode and change lib to ["ES2022", "ESNext.Decorators", "DOM"]. The runtime must provide Symbol.metadata before the decorated class is evaluated; this standalone example installs a minimal fallback if needed:
if (!Symbol.metadata) {
Object.defineProperty(Symbol, "metadata", {
value: Symbol("Symbol.metadata"),
});
}
const labelKey = Symbol("label");
function label(_value: unknown, context: ClassDecoratorContext): void {
context.metadata[labelKey] = "greeter";
}
@label
class Greeter {}
console.log(Greeter[Symbol.metadata]?.[labelKey]);greeterIn an application, arrange that initialization centrally before importing decorated modules. Adding a compiler library supplies types, not runtime implementations; see the TypeScript 5.2 metadata documentation for the compilation and runtime requirements.
Execution Timing, Ordering, and Migration
Definition Time Is Different From Call Time
Decorator functions run while a class definition is evaluated. A returned method wrapper runs on method calls; a standard field decorator can instead return an initializer that runs when the field is initialized. Standard decorators also provide context.addInitializer for scheduled initialization.
For decorators stacked on one declaration, factory expressions evaluate top-to-bottom and the resulting decorators apply bottom-to-top. With @outer() above @inner(), the result is conceptually outer(inner(method)). Cross-member ordering differs between the systems: legacy decoration processes instance members before static members, so don't migrate code that depends on registration side effects without checking its ordering.
Framework Compatibility Determines Your Migration Path
Don't disable legacy mode just because you're upgrading TypeScript. The official NestJS TypeScript starter enables both legacy decorators and emitted metadata. Nest uses runtime class tokens for dependency resolution; interfaces need explicit tokens, as its provider documentation explains.
- Inventory decorators and the code that consumes their metadata, including your test and production build pipelines.
- Replace descriptor-based wrappers with value/context implementations where dependencies support standard decorators.
- Redesign parameter annotations and emitted-type dependencies using supported framework APIs, explicit injection, or explicit schemas.
- Test construction, inheritance, decorator composition, and asynchronous failures before switching modes.
TypeScript Decorator Use Cases
A cross-cutting concern is behavior needed across otherwise unrelated application features. Decorators earn their place when the same policy belongs on many class members and an annotation makes that policy easier to see.
- Logging and error reporting: Wrap calls to record operation names and failures. Redact sensitive arguments and rethrow errors unless recovery is an explicit contract.
- Caching: Wrap lookup, execution on a miss, and storage. Define tenant-aware keys, expiration, bounded storage, and invalidation; a decorator alone supplies none of those policies.
- Monitoring: Measure successful and failed operations, awaiting asynchronous completion. You still need a metrics or tracing exporter.
- Business rules: Use decorators for uniform policies, not to hide domain decisions that callers need to understand.
- Data validation: Invoke a runtime validator or record rules that another component consumes. Attaching metadata alone does not validate input.
TypeScript Decorators and AOP
In aspect-oriented programming, a join point is an execution event, such as a method execution. Advice is behavior that runs before, after, or around it; a pointcut selects join points, and weaving connects the advice to them. The AspectJ programming guide explains that terminology.
A logging method decorator implements a small, explicit form of around advice. TypeScript decorators aren't a general-purpose AOP engine: they don't provide a pointcut language or automatically intercept every function call. For a single operation, an ordinary wrapper function is often clearer; explicit constructor injection is easier to trace when dependencies matter more than annotation syntax.
Common Mistakes
- Mixing decorator systems: Changing a compiler flag doesn't rewrite decorator implementations or metadata consumers.
- Losing the receiver: A replacement method should forward the call's
this, rather than capture an unrelated lexical receiver. - Ignoring asynchronous failures: Synchronous try/catch doesn't catch a later promise rejection.
- Sharing state accidentally: A closure created during decoration is shared by calls to that decorated method. Scope caches deliberately when data belongs to an instance or tenant.
- Treating composition as cosmetic: Authorization outside a cache wrapper and authorization inside it can have different security consequences. Test the actual order.
Conclusion
Choose standard decorators for new code when your dependencies support them. Keep legacy decorators where framework contracts require parameter annotations or emitted design metadata. In either system, prefer a decorator when it makes repeated policy visible—and prefer an ordinary function when it makes control flow clearer.
FAQ
What is the difference between standard and legacy TypeScript decorators?
Standard decorators receive a value and a context object. Legacy decorators use declaration-specific arguments such as target, propertyKey, and descriptor, and are selected by experimentalDecorators: true.
Do TypeScript decorators require experimentalDecorators?
Only legacy decorators require experimentalDecorators: true. Standard decorators, supported since TypeScript 5.0, work with that option unset or false.
Do standard TypeScript decorators support parameter decorators?
No. Parameter decorators belong to the legacy system; adopting standard decorators requires replacing parameter-based annotation patterns with another supported mechanism.
What does emitDecoratorMetadata do, and when is it needed?
It emits limited design-type metadata for decorated declarations in legacy mode, commonly consumed through reflect-metadata. Enable it when a dependency needs that information; it is not required for decorators or explicit custom metadata and does not preserve complete TypeScript types.
How do I write a standard TypeScript method decorator?
Accept the original method and a ClassMethodDecoratorContext, then return a replacement function. Forward the receiver, arguments, return value, and errors; use an async-aware wrapper when behavior depends on promise completion.
Further Reading
- TypeScript 5.0 decorators — standard signatures and legacy compatibility.
- Legacy decorator handbook — class, method, accessor, property, and parameter examples.
- TC39 decorators proposal — replacement semantics and initialization timing.
- TypeScript 5.2 decorator metadata — context.metadata and runtime setup.
- reflect-metadata documentation — legacy metadata storage and lookup.
- emitDecoratorMetadata reference — compiler-generated design metadata.
- NestJS providers — dependency injection and runtime tokens.
- AspectJ programming guide — join points, pointcuts, advice, and weaving.
