TypeScript Decorators: Class, Method, and Property Decorators Guide
TypeScript decorators provide a declarative way to annotate and modify the behavior of classes, methods, properties, and parameters at design time. Originally proposed for a future ECMAScript standard, decorators are a stage-2 TC39 proposal that TypeScript has supported experimentally since version 1.5. They enable metaprogramming patterns — such as logging, access control, dependency injection, and validation — without cluttering core business logic. Microsoft’s TypeScript team maintains decorator support through the experimentalDecorators compiler option (see TypeScript Decorators Handbook). This guide explores each decorator type with practical, production-grade examples.
Class Decorators
A class decorator is a function applied to a class constructor. It can observe, modify, or replace the constructor definition. The decorator receives the constructor as its sole argument and may return a modified constructor or void.
function sealed<T extends { new (...args: any[]): {} }>(constructor: T) {
Object.seal(constructor);
Object.seal(constructor.prototype);
---Applying @sealed prevents runtime extension of the class’s prototype and static members. Class decorators are commonly used in inversion-of-control containers and ORMs. For example, TypeORM uses @Entity() to register classes as database tables, and Angular’s @Component() wires up templates, styles, and dependency injection. When a class decorator returns a new constructor, it replaces the original — enabling proxy patterns, singleton enforcement, or timing instrumentation.
Singleton via Class Decorator
function singleton<T extends { new (...args: any[]): {} }>(ctor: T) {
let instance: InstanceType<T>;
return new Proxy(ctor, {
construct(target, args) {
if (!instance) instance = new target(...args);
return instance;
}
});
---This pattern ensures only one instance exists per decorated class, a clean alternative to manual singleton boilerplate.
Method Decorators
Method decorators receive three arguments: the target prototype, the method name, and a property descriptor. They can log execution times, validate arguments, enforce permissions, or memoize results. The return value, if provided, replaces the property descriptor.
Logging Decorator
function log(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${key} with`, args);
const result = original.apply(this, args);
console.log(`${key} returned`, result);
return result;
};
---This is invaluable for debugging legacy codebases where adding logging statements to every method would be invasive. For production, you might integrate with structured logging libraries like Winston or Pino.
Memoization Decorator
function memoize(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
const cache = new Map<string, any>();
descriptor.value = function (...args: any[]) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = original.apply(this, args);
cache.set(key, result);
return result;
};
---Expensive computations — such as Fibonacci, data transformations, or API response aggregations — benefit greatly from memoization.
Property Decorators
Property decorators intercept property declarations. They receive the target prototype and property name but no property descriptor (property descriptors are unavailable during class definition). They are useful for adding metadata or side effects, such as marking a property for serialization or validation.
function required(target: any, key: string) {
const existing = Reflect.getOwnMetadata('required', target) ?? [];
Reflect.defineMetadata('required', [...existing, key], target);
---Using reflect-metadata (a polyfill for the Reflect Metadata proposal), property decorators feed information into a runtime validation system. Libraries like class-validator use this pattern extensively to define constraints:
class User {
@required
email: string;
---A validation function reads the required metadata array and checks each property at runtime.
Accessor Decorators
Accessor decorators apply to getters and setters, receiving the same signature as method decorators. They can intercept property access, implement lazy initialization, or add change tracking.
function configurable(value: boolean) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
};
---Lazy initialization via accessor decorators is especially useful for expensive singleton properties:
function lazyInit<T>(factory: () => T) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.get;
descriptor.get = function () {
if (!this.hasOwnProperty(key)) {
this['_' + key] = factory();
}
return this['_' + key];
};
};
---Parameter Decorators
Parameter decorators annotate individual constructor or method parameters. They receive the target, the method name, and the parameter index. Combined with method decorators and metadata, they enable dependency injection frameworks like InversifyJS and NestJS to resolve constructor parameters automatically.
Real-World Injection Pattern
In a typical NestJS service, the @Inject() decorator tells the DI container which dependency to provide:
@Injectable()
export class UsersService {
constructor(
@Inject('DATABASE_CONNECTION') private db: Database,
@Inject(ConfigService) private config: ConfigService,
) {}
---Behind the scenes, NestJS reads the design:paramtypes metadata emitted by the TypeScript compiler and cross-references it with the injection tokens stored by parameter decorators. When emitDecoratorMetadata is enabled, the compiler records each parameter’s type, allowing the framework to resolve dependencies without explicit providers in many cases.
function inject(serviceIdentifier: string) {
return function (target: Object, key: string | symbol, index: number) {
const existing = Reflect.getOwnMetadata('design:paramtypes', target, key) ?? [];
// store injection token for later resolution
Reflect.defineMetadata(`inject:${key}:${index}`, serviceIdentifier, target);
};
---Accessor Decorators in Depth
Accessor decorators are particularly powerful for lazy loading and change detection. Beyond simple lazy initialization, they can implement observable properties that emit events when values change:
function observable<T>(target: any, key: string, descriptor: PropertyDescriptor) {
const privateKey = `_${key}`;
const listeners: Set<(value: T) => void> = new Set();
descriptor.get = function () {
return this[privateKey];
};
descriptor.set = function (value: T) {
const old = this[privateKey];
this[privateKey] = value;
if (old !== value) {
listeners.forEach(cb => cb(value));
}
};
// attach subscribe method to the instance
target[key + '_subscribe'] = (cb: (value: T) => void) => {
listeners.add(cb);
return () => listeners.delete(cb);
};
---This pattern is the basis for reactive state management in libraries like MobX and Vue, where property changes automatically trigger re-renders or side effects.
Execution Order and Composition
Understanding the order in which decorators execute is critical for complex applications. When multiple decorators are stacked, evaluation happens bottom-up (from nearest the declaration outward), while execution happens top-down:
@classDecorator
class Example {
@propertyDecorator
@methodDecorator
method() {}
---Evaluation order: methodDecorator first, then propertyDecorator, then classDecorator. Execution order (when the decorated method runs): classDecorator wraps first, then propertyDecorator, then methodDecorator. This reversed execution chain is analogous to middleware in Express or Redux — each decorator can intercept, modify, or short-circuit the call.
Decorator Factories
function validate(minLength: number) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
for (const arg of args) {
if (typeof arg === 'string' && arg.length < minLength) {
throw new Error(`Argument too short (min ${minLength})`);
}
}
return original.apply(this, args);
};
};
---Usage: @validate(3) on a method enforces that all string arguments are at least three characters long.
Metadata Reflection and Real-World Usage
The emitDecoratorMetadata compiler option, combined with reflect-metadata, enables decorators to inspect types at runtime. When enabled, TypeScript emits design:type, design:paramtypes, and design:returntype metadata. This is the foundation of Angular’s dependency injection and NestJS’s controller parameter resolution.
For example, NestJS’s @Controller() and @Get() use decorators to map HTTP routes to methods, injecting request parameters automatically:
@Controller('users')
export class UsersController {
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(id);
}
---Modern frameworks like TypeGraphQL and Routing Controllers also rely heavily on decorators for declarative API definitions.
TC39 Stage 3 Decorators vs TypeScript Experimental Decorators
TypeScript’s experimental decorators (enabled via experimentalDecorators) follow an older proposal that differs significantly from the current TC39 Stage 3 specification. The key differences:
| Feature | TypeScript Experimental | TC39 Stage 3 |
|---|---|---|
| Context argument | No — fixed signature | Yes — flexible context object |
| Class decorator return | New constructor | New constructor or void |
| Method decorator | Three args (target, key, desc) | One arg (value, context) |
| Property decorator | Two args (target, key) | One arg (value, context) |
TypeScript 5.x introduced support for TC39 decorators behind the customDecorators flag, but the ecosystem (Angular, NestJS, TypeORM, class-validator) still depends on experimental decorators. When migrating to standard decorators, expect breaking changes in how metadata is accessed and how decorator factories are structured.
Using Decorators Without Experimental Mode
If you prefer not to enable experimentalDecorators, you can achieve many decorator-like patterns using higher-order functions and wrapper patterns:
function withLogging<T extends (...args: any[]) => any>(fn: T): T {
return ((...args: Parameters<T>) => {
console.log(`Calling ${fn.name} with`, args);
const result = fn(...args);
console.log(`${fn.name} returned`, result);
return result;
}) as T;
---This functional approach works in plain JavaScript and future-proofs your code against the evolving decorator standard. However, it lacks the declarative @syntax and cannot be applied to classes or properties as cleanly.
Pitfalls and Limitations
- Experimental Status: Decorators require
experimentalDecorators: truein tsconfig. The TC39 proposal has evolved significantly; the current Stage-3 specification differs from TypeScript’s implementation. TypeScript 5.x introduced custom decorator support aligned with the TC39 spec under thecustomDecoratorsflag, but the legacy experimental decorators remain the most widely used. - No Property Descriptor for Property Decorators: Unlike method decorators, property decorators receive no descriptor, which limits what they can intercept.
- Order of Evaluation: Decorators apply in reverse order: parameter decorators → property/accessor/method decorators → class decorators. Understanding this order prevents subtle bugs.
- Tree-Shaking Concerns: Decorator metadata can prevent dead-code elimination in bundlers, potentially increasing bundle size.
FAQ
Do TypeScript decorators work with arrow function properties?
No. Method decorators only apply to method declarations, not arrow function class properties. Use regular method syntax for decorated members.
Can I use multiple decorators on a single declaration?
Yes. Decorators compose: @sealed @log applies log first (nearest the declaration), then sealed. The result of one decorator feeds into the next.
Are decorators available in plain JavaScript?
Not yet. The TC39 proposal is at Stage 3. TypeScript’s experimental decorators predate the standard and are likely to diverge. Once ECMAScript decorators ship, TypeScript will align with the spec.
How do I test a decorated class?
Test the class as you normally would. Decorators are applied at declaration time, so decorated classes behave consistently. For unit tests, you can mock decorators by exporting them and replacing them in test setup.
What is the relationship between decorators and mixins?
Both decorators and mixins support code reuse. Mixins compose functionality via class extension; decorators modify behavior inline. In TypeScript, mixins often employ class decorators or helper functions like applyMixins as described in the TypeScript Mixins guide.