TypeScript 77 ๐ท Decorators โ Legacy Experimental
Decorators are a language feature that attaches metadata or behavior to classes, methods, properties, and parameters. TypeScript has had two versions of this feature: the legacy experimental version, which required the experimentalDecorators compiler flag and was never standardized, and the standard Stage 3 version, which TypeScript 5.0 shipped as the default. The legacy version is the one this chapter covers. It is the version that NestJS, TypeORM, Angular (through v16), and class-validator depend on, and it is the version most production code still uses. Understanding it means understanding the runtime behavior, the compiler flags, the metadata system that powers dependency injection, and the specific syntax that the Stage 3 decorators replaced.
Key point: Legacy decorators require "experimentalDecorators": true in tsconfig.json. They are applied with the syntax @expression, evaluated bottom-up for each declaration, and called with either three arguments (method, accessor decorators: target, propertyKey, descriptor) or two arguments (property decorators: target, propertyKey) . Parameter decorators receive three arguments: target, propertyKey, parameterIndex . The emitDecoratorMetadata flag emits runtime type metadata (design:type, design:paramtypes, design:returntype) using the reflect-metadata library, which is what allows dependency injection frameworks to resolve constructor parameter types .
Enabling legacy decorators
Legacy decorators are disabled by default. The compiler flag must be set, either on the command line or in tsconfig.json.
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
The experimentalDecorators flag enables the syntax and the runtime emission. The emitDecoratorMetadata flag enables the metadata reflection, which requires the reflect-metadata package to be installed and imported at the application entry point . Without the import, Reflect.getMetadata is undefined and the metadata calls fail at runtime.
Why the flag exists. The decorator proposal was never finalized in the form TypeScript implemented. The experimentalDecorators flag is a marker that the feature is not stable and may change. TypeScript 5.0 shipped the Stage 3 proposal as the default, and the legacy version is now the opt-in behavior .
Why emitDecoratorMetadata matters for frameworks. NestJS, TypeORM, and class-validator rely on the metadata to know what types a constructor’s parameters have. When @Injectable() is applied to a class, the compiler emits metadata that says “the first parameter is of type UserService.” The DI container reads this metadata and resolves the dependency . Without emitDecoratorMetadata, the metadata is not emitted, and the framework cannot resolve the dependencies.
Why the metadata is controversial. The metadata emission changes the runtime code based on type information, which violates TypeScript’s design goal of types being erased and having no runtime effect . The types are not available at runtime, so the compiler has to synthesize the metadata at compile time, and the emitted code depends on what the compiler inferred. This is why the Stage 3 decorators removed the metadata feature entirely.
Method decorators
A method decorator is applied to a method and receives three arguments: the target (the prototype for an instance method, the constructor for a static method), the property key, and the property descriptor .
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor): void {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with`, args);
return original.apply(this, args);
};
}
class Calculator {
@log
add(a: number, b: number): number {
return a + b;
}
}
The log decorator replaces the method’s value with a wrapper that logs and delegates. The @log syntax applies the decorator at class definition time, and the wrapper runs on every call .
Why the descriptor is the mechanism. The method decorator receives the PropertyDescriptor, which is the same object that Object.defineProperty uses. The decorator can modify value, enumerable, configurable, and writable. This is how logging, timing, memoization, and validation wrappers are implemented .
Why the return value matters. If the decorator returns a value, that value is used as the new PropertyDescriptor. Returning undefined means the original descriptor (possibly modified in place) is used . The common pattern is to mutate the descriptor in place and return nothing, or to return a new descriptor.
Why the any types are the legacy signature. The legacy decorator signature uses any for the target and string for the property key. There is no type safety at the decorator boundary. The Stage 3 decorators replaced this with a typed context object .
Property decorators
A property decorator is applied to a property and receives two arguments: the target and the property key. It does not receive a property descriptor .
function format(formatString: string) {
return function (target: any, propertyKey: string): void {
Reflect.defineMetadata("format", formatString, target, propertyKey);
};
}
class Greeter {
@format("Hello, %s")
greeting: string;
greet(): string {
const formatString = Reflect.getMetadata("format", this, "greeting");
return formatString.replace("%s", this.greeting);
}
}
The format factory returns a property decorator that stores metadata on the class prototype. The greet method reads the metadata at runtime and uses it .
Why property decorators cannot modify behavior directly. There is no property descriptor for an instance property at class definition time. The property is defined on the instance, not the prototype, and the decorator runs before any instance exists. The decorator can only observe that a property of that name has been declared, and it can store metadata. Behavior modification requires Object.defineProperty at a later point, or a getter/setter .
Why property decorators are mostly for metadata. Because they cannot intercept reads or writes directly, their primary use is storing metadata that other code reads. Dependency injection frameworks use property decorators for @Inject() and similar markers, and validation frameworks use them for @IsEmail() and similar constraints .
Why the metadata reflection matters. The Reflect.defineMetadata and Reflect.getMetadata calls require reflect-metadata to be imported. Without it, the methods are undefined and the decorator throws at runtime .
Accessor decorators
An accessor decorator is applied to a getter or setter. It receives three arguments, like a method decorator, because accessors have property descriptors .
function configurable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): void {
descriptor.configurable = value;
};
}
class Point {
private _x: number = 0;
@configurable(false)
get x(): number {
return this._x;
}
}
The configurable decorator modifies the descriptor’s configurable flag. The decorator is applied to the first accessor in document order if both a getter and a setter exist for the same member .
Why only one accessor is decorated. TypeScript applies the decorator to the combined descriptor for the property, not to the getter and setter separately. If both are declared, the decorators must be applied to the first one in source order .
Parameter decorators
A parameter decorator is applied to a constructor parameter or a method parameter. It receives three arguments: the target, the property key (or undefined for a constructor), and the parameter index .
function inject(token: string) {
return function (target: any, propertyKey: string | undefined, parameterIndex: number): void {
const existing = Reflect.getOwnMetadata("inject", target, propertyKey) || {};
existing[parameterIndex] = token;
Reflect.defineMetadata("inject", existing, target, propertyKey);
};
}
class UserService {
constructor(@inject("Database") private db: any) {}
}
The inject factory returns a parameter decorator that stores the token under the parameter index. At runtime, the DI container reads this metadata and resolves the parameter .
Why parameter decorators are the DI backbone. NestJS and Angular (legacy) use parameter decorators to mark constructor parameters for injection. The decorator stores the token, and the framework reads the metadata to know what to inject . The emitDecoratorMetadata flag provides the type information when no explicit token is given, which is why the two features are used together.
Why parameter decorators are not supported in Stage 3. The standard proposal has not included parameter decorators. This is the primary reason NestJS and TypeORM cannot migrate to Stage 3 yet .
Decorator evaluation order
When multiple decorators are applied to a single declaration, they are evaluated in a specific order: the expressions are evaluated top-down, but the resulting decorator functions are called bottom-up .
@first
@second
method() {}
first is evaluated first, then second. But second is called first, then first. The result is that first wraps second .
Why bottom-up matters. If both decorators modify the method, the one closest to the method (the bottom one) runs first, and the outer one sees the result. This is the same order as function composition: first(second(method)).
Why the order matters for frameworks. When a class has both a class decorator and a method decorator, the method decorators run first (bottom-up), then the class decorator. The class decorator sees the fully decorated class. This is why @Injectable() on a class and @Inject() on a parameter work together: the parameter decorator stores the metadata, and the class decorator reads it.
Complete Example Session
// ============================================
// PART 1: THE TSCONFIG
// ============================================
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
// ============================================
// PART 2: THE METHOD DECORATOR
// ============================================
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor): void {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey}`, args);
return original.apply(this, args);
};
}
class Calculator {
@log
add(a: number, b: number): number {
return a + b;
}
}
// ============================================
// PART 3: THE PROPERTY DECORATOR
// ============================================
function format(formatString: string) {
return function (target: any, propertyKey: string): void {
Reflect.defineMetadata("format", formatString, target, propertyKey);
};
}
class Greeter {
@format("Hello, %s")
greeting: string = "World";
}
// ============================================
// PART 4: THE PARAMETER DECORATOR
// ============================================
function inject(token: string) {
return function (target: any, propertyKey: string | undefined, parameterIndex: number): void {
console.log(`Injecting ${token} at index ${parameterIndex}`);
};
}
class UserService {
constructor(@inject("Database") private db: any) {}
}
// ============================================
// PART 5: THE METADATA
// ============================================
// With emitDecoratorMetadata, the compiler emits:
// __metadata("design:type", Function)
// __metadata("design:paramtypes", [Object])
// __metadata("design:returntype", void 0)
// The metadata is readable with:
Reflect.getMetadata("design:paramtypes", UserService);
// ============================================
// PART 6: THE EVALUATION ORDER
// ============================================
// The source:
@first
@second
method() {}
// The evaluation:
// 1. first is evaluated
// 2. second is evaluated
// 3. second is called
// 4. first is called
// ============================================
// PART 7: THE TYPICAL FRAMEWORK USE
// ============================================
// NestJS-style:
@Injectable()
class UserService {
constructor(
@InjectRepository(User) private repo: Repository<User>,
private logger: LoggerService,
) {}
}
// The @Injectable() class decorator marks the class.
// The @InjectRepository() parameter decorator marks the parameter.
// The emitDecoratorMetadata provides the type for the logger.
// ============================================
// PART 8: THE REFLECT-METADATA IMPORT
// ============================================
// main.ts
import "reflect-metadata";
// Must be imported before any decorated class is defined.
// ============================================
// PART 9: THE STAGE 3 COMPARISON
// ============================================
// Legacy (this chapter):
@log
method() {}
// Stage 3 (TypeScript 5.0+ default):
@log
method() {}
// The syntax is the same, but the signature differs:
// Legacy: (target, propertyKey, descriptor)
// Stage 3: (value, context)
// ============================================
// PART 10: WHAT NOT TO DO
// ============================================
// Don't forget the experimentalDecorators flag
@log
method() {} // โ without the flag
// Don't forget the reflect-metadata import
Reflect.getMetadata(...) // โ without the import
// Don't use parameter decorators with Stage 3
constructor(@inject("DB") db: any) {} // โ Stage 3 does not support this
// Don't use the legacy decorators in new projects without a reason
// The Stage 3 decorators are the default.
The ten parts cover the tsconfig, the method decorator, the property decorator, the parameter decorator, the metadata, the evaluation order, the framework use, the reflect-metadata import, the Stage 3 comparison, and the anti-patterns.
Quick Reference
The Decorator Types
| Type | Arguments | Can Modify Behavior |
|---|---|---|
| Method | target, propertyKey, descriptor | โ |
| Accessor | target, propertyKey, descriptor | โ |
| Property | target, propertyKey | โ (metadata only) |
| Parameter | target, propertyKey, parameterIndex | โ (metadata only) |
| Class | constructor | โ |
The Compiler Flags
| Flag | Purpose |
|---|---|
experimentalDecorators | Enable legacy decorators |
emitDecoratorMetadata | Emit runtime type metadata |
reflect-metadata | The runtime library (npm) |
The Metadata Keys
| Key | Purpose |
|---|---|
design:type | The property’s type |
design:paramtypes | The method’s parameter types |
design:returntype | The method’s return type |
The Frameworks Using Legacy Decorators
| Framework | Use |
|---|---|
| NestJS | @Injectable(), @Controller(), @Inject() |
| TypeORM | @Entity(), @Column(), @ManyToOne() |
| Angular (pre-v16) | @Component(), @Injectable(), @Input() |
| class-validator | @IsEmail(), @MinLength() |
The Evaluation Order
| Step | Action |
|---|---|
| 1 | Top-down expression evaluation |
| 2 | Bottom-up decorator application |
| 3 | Class decorator runs last |
Best Practices
โ Do This:
// Enable both flags for legacy decorators
{
"experimentalDecorators": true,
"emitDecoratorMetadata": true
} // โ
// Import reflect-metadata at the entry point
import "reflect-metadata"; // โ
// Use decorator factories for configurable decorators
function log(prefix: string) { return (target: any, key: string, desc: PropertyDescriptor) => { ... }; } // โ
// Keep decorators focused on one responsibility
function log(target: any, key: string, desc: PropertyDescriptor) { ... } // โ
// Use parameter decorators for DI in legacy frameworks
constructor(@inject("Database") private db: any) {} // โ
โ Don’t Do This:
// Don't use decorators without the flag
@log
method() {} // โ the experimentalDecorators flag is required // โ ๏ธ
// Don't forget the reflect-metadata import
Reflect.getMetadata(...) // โ the library is not imported // โ ๏ธ
// Don't use parameter decorators with Stage 3
constructor(@inject("DB") db: any) {} // โ the Stage 3 does not support it // โ ๏ธ
// Don't use the legacy decorators in new projects without a reason
// The Stage 3 decorators are the default. // โ ๏ธ
// Don't rely on the metadata for the complex types
// The emitted metadata is limited to the design types. // โ ๏ธ
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
Missing experimentalDecorators | The syntax error | The flag |
Missing emitDecoratorMetadata | The DI fails | The flag |
Missing reflect-metadata import | The runtime error | The import |
| The parameter decorator in Stage 3 | The error | The legacy |
| The circular dependency | The metadata resolution fails | The forwardRef |
| The property decorator cannot modify | The no-effect | The metadata |
| The evaluation order confusion | The wrong wrap | The bottom-up |
Real-World Examples
1. The @Injectable()
@Injectable()
class UserService {}
2. The @Inject()
constructor(@Inject("Database") private db: Database) {}
3. The @Entity()
@Entity()
class User {
@PrimaryKey()
id!: number;
@Column()
name!: string;
}
4. The @Component()
@Component({ selector: 'app-root' })
class AppComponent {}
5. The @Input()
@Input() name: string;
6. The @IsEmail()
@IsEmail()
email: string;
7. The custom method decorator
function log(target: any, key: string, desc: PropertyDescriptor) { ... }
8. The custom property decorator
function format(fmt: string) { return (target: any, key: string) => { ... }; }
9. The custom parameter decorator
function inject(token: string) { return (target: any, key: string, index: number) => { ... }; }
10. The reflect-metadata import
import "reflect-metadata";
Visual: The Decorator Types and Their Arguments
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ METHOD DECORATOR โ
โ @log โ
โ method() {} โ
โ โ
โ (target, propertyKey, descriptor) โ
โ โ Can modify behavior โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ PROPERTY DECORATOR โ
โ @format("Hello, %s") โ
โ greeting: string; โ
โ โ
โ (target, propertyKey) โ
โ โ Metadata only โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ PARAMETER DECORATOR โ
โ constructor(@inject("DB") db) {} โ
โ โ
โ (target, propertyKey, parameterIndex) โ
โ โ Metadata only โ
โ โ
โ The legacy signature uses any/string. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: The Evaluation Order
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE SOURCE: โ
โ @first โ
โ @second โ
โ method() {} โ
โ โ
โ THE EVALUATION: โ
โ 1. first is evaluated (top-down) โ
โ 2. second is evaluated โ
โ โ
โ THE APPLICATION: โ
โ 3. second is called (bottom-up) โ
โ 4. first is called โ
โ โ
โ THE RESULT: โ
โ first(second(method)) โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: The Metadata Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE SOURCE: โ
โ @Injectable() โ
โ class UserService { โ
โ constructor(private logger: Logger) {} โ
โ } โ
โ โ
โ โ The compiler โ
โ โผ โ
โ โ
โ THE EMITTED: โ
โ __metadata("design:paramtypes", [Logger]) โ
โ โ
โ โ The runtime โ
โ โผ โ
โ โ
โ THE DI CONTAINER: โ
โ Reflect.getMetadata(...) โ
โ โ resolves the Logger โ
โ โ
โ The metadata bridges the compile-time's โ
โ types and the runtime's DI. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: The Legacy vs the Stage 3
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE LEGACY (the experimental) โ
โ (target, propertyKey, descriptor) โ
โ The parameter decorators's supported โ
โ The emitDecoratorMetadata's supported โ
โ The experimentalDecorators flag โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ THE STAGE 3 (the modern) โ
โ (value, context) โ
โ The parameter decorators's NOT supported โ
โ The metadata's NOT supported โ
โ The no flag's required โ
โ โ
โ The legacy's is the framework's, and the โ
โ Stage 3's is the future's. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: The Framework Dependencies
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ NestJS / TypeORM / class-validator โ
โ The legacy decorators's โ
โ The emitDecoratorMetadata's โ
โ The reflect-metadata's โ
โ The parameter decorators's โ
โ โ
โ THE MIGRATION'S BLOCKER: โ
โ The Stage 3's has no parameter decorators.โ
โ The Stage 3's has no metadata's. โ
โ โ
โ The frameworks's cannot migrate yet. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| The flag | experimentalDecorators: true |
| The metadata flag | emitDecoratorMetadata: true |
| The library | reflect-metadata |
| The method’s args | (target, propertyKey, descriptor) |
| The property’s args | (target, propertyKey) |
| The parameter’s args | (target, propertyKey, parameterIndex) |
| The evaluation | The bottom-up |
| The framework | NestJS, TypeORM, Angular |
| The Stage 3 | The default in TS 5.0+ |
| The parameter decorators | The legacy only |
Key takeaways:
- The legacy decorators require the
experimentalDecoratorsflag โ the syntax is disabled by default, and the flag enables both the parsing and the runtime emission - The
emitDecoratorMetadataflag emits runtime type metadata โ thedesign:type, thedesign:paramtypes, and thedesign:returntypeare the three keys, and thereflect-metadatapackage is required - The method and accessor decorators receive three arguments โ the
target, thepropertyKey, and thedescriptor, and they can modify the behavior - The property decorators receive two arguments โ the
targetand thepropertyKey, and they can only store metadata, not modify behavior - The parameter decorators receive three arguments โ the
target, thepropertyKey, and theparameterIndex, and they are the DI framework’s backbone - The decorators are evaluated top-down and applied bottom-up โ the bottom decorator runs first, and the top decorator sees the result
- The legacy decorators are the framework’s requirement โ the NestJS, the TypeORM, the class-validator depend on the parameter decorators and the metadata, and the Stage 3 does not support them
- The Stage 3 decorators are the default in TypeScript 5.0+ โ the
(value, context)signature, the no flags, the no metadata, and the no parameter decorators - The
reflect-metadataimport is mandatory โ theReflect.getMetadataand theReflect.defineMetadataare undefined without it, and the decorated class throws at runtime - The metadata emission changes the runtime behavior based on type information โ this is the reason the Stage 3 removed the feature, and it is the legacy’s fundamental’s tradeoff
Remember: The legacy experimental decorators are the TypeScript’s pre-standard’s feature, and they are the framework’s dependency’s. The experimentalDecorators and the emitDecoratorMetadata are the flags, and the reflect-metadata is the library. The method’s and the property’s and the parameter’s decorators have the specific’s signatures. The evaluation’s is the bottom-up, and the metadata’s is the DI’s. The Stage 3’s is the future’s, but the legacy’s is the present’s for the NestJS and the TypeORM.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!