Decorators in TypeScript
Decorators are one of TypeScript’s most powerful features for metaprogramming. They let you add metadata and modify the behavior of classes, methods, accessors, and properties without changing their actual code. If you’ve used annotations in Java or attributes in C#, you’ll find decorators familiar—though TypeScript’s implementation is more flexible.
In this tutorial, you’ll learn what decorators are, how they work in TypeScript 5.0+, and how to use them effectively in your projects.
Enabling Decorators in TypeScript
Before you can use decorators, you need to enable them in your TypeScript configuration. Add the experimentalDecorators option to your tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
The experimentalDecorators option enables the decorator syntax. The emitDecoratorMetadata option is optional—it emits type metadata for decorators that use reflection, which is useful when combined with libraries like TypeORM or routing frameworks.
Understanding Decorator Functions
At their core, decorators are functions that get called by TypeScript at runtime. When you write @decoratorName, TypeScript calls your function with information about what you’re decorating.
Here’s a simple class decorator:
function logged<T extends Function>(constructor: T) {
console.log(`Creating instance of: ${constructor.name}`);
return constructor;
}
@logged
class User {
constructor(public name: string) {}
}
const user = new User("Alice");
// Output: Creating instance of: User
The decorator function receives the class constructor as its argument. You can use this to observe, modify, or replace the class definition entirely.
Decorator Factories for Customization
Plain decorators like the one above are useful, but often you need to configure how a decorator behaves. That’s where decorator factories come in—a factory is a function that returns a decorator function.
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
function printable(value: string) {
return function(target: Function) {
target.prototype.printFormat = value;
};
}
@sealed
@printable("PDF")
class Document {
constructor(public title: string) {}
}
console.log((new Document("Report") as any).printFormat);
// Output: PDF
Decorator factories let you pass arguments to customize behavior. The factory returns the actual decorator function that gets applied to the class.
Method Decorators
Method decorators let you wrap or modify methods. They receive three arguments: the target (class prototype for instance methods, constructor for static methods), the method name, and the property descriptor.
function readonly(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.writable = false;
}
class Calculator {
@readonly
static VERSION = "1.0.0";
@readonly
add(a: number, b: number): number {
return a + b;
}
}
const calc = new Calculator();
// This would fail because the method is read-only:
// calc.add = function() { return 0; };
This decorator makes a method immutable after definition. You can also use method decorators to add logging, timing, or validation logic around method calls.
Accessor Decorators
Accessors (getters and setters) can also be decorated. TypeScript applies decorators to the first accessor in document order, since the property descriptor combines both get and set.
function validated(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalSetter = descriptor.set;
descriptor.set = function(value: number) {
if (value < 0) {
throw new Error(`${propertyKey} cannot be negative`);
}
originalSetter?.call(this, value);
};
}
class Account {
private _balance: number = 0;
@validated
set balance(value: number) {
this._balance = value;
}
get balance(): number {
return this._balance;
}
}
const account = new Account();
account.balance = 100; // Works
account.balance = -50; // Throws: balance cannot be negative
Accessor decorators are powerful for adding validation or transforming values before they’re stored.
Property Decorators
Property decorators receive two arguments: the target and the property name. Unlike method and accessor decorators, property decorators don’t receive a property descriptor because there’s no standard way to describe instance properties in ES5.
function formatDate(target: any, propertyKey: string) {
const symbol = Symbol(propertyKey);
Object.defineProperty(target, propertyKey, {
get() {
return new Date(this[symbol]).toLocaleDateString();
},
set(value: string) {
this[symbol] = value;
}
});
}
class Event {
@formatDate
date: string = "2026-03-07";
}
const event = new Event();
console.log(event.date); // Output: 3/7/2026 (depending on locale)
Property decorators work with Object.defineProperty to intercept getter and setter behavior for class properties.
Parameter Decorators
Parameter decorators let you add metadata to function parameters. They’re commonly used for dependency injection or validation frameworks.
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
const parameterKeys = Symbol("requiredParameters");
if (!target[parameterKeys]) {
target[parameterKeys] = [];
}
target[parameterKeys].push(parameterIndex);
}
class UserService {
createUser(@required name: string, @required email: string) {
console.log(`Creating user: ${name} (${email})`);
}
}
const service = new UserService();
service.createUser("Alice", "alice@example.com");
// Output: Creating user: Alice (alice@example.com)
Parameter decorators don’t prevent the function from being called—you’d need to combine them with method decorators to actually validate parameters before invocation.
Decorator Composition and Evaluation Order
When multiple decorators apply to a single declaration, they evaluate in a specific order. Factory expressions are evaluated top-to-bottom, but the decorators themselves execute bottom-to-top.
function first() {
console.log("first(): factory evaluated");
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("first(): called");
};
}
function second() {
console.log("second(): factory evaluated");
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("second(): called");
};
}
class Example {
@first()
@second()
method() {}
}
// Output:
// first(): factory evaluated
// second(): factory evaluated
// second(): called
// first(): called
Understanding this order matters when building composed decorators where execution sequence affects behavior.
Practical Use Cases
Decorators shine in real-world scenarios. Popular frameworks use them extensively. Angular relies on decorators for components, services, and dependency injection. TypeORM uses them to define database entities. NestJS builds its entire architecture around decorator-based patterns.
Common practical applications include:
- Validation: Check that values meet certain criteria before assignment or method execution
- Logging: Track method calls, arguments, and return values
- Timing: Measure how long operations take
- Caching: Store and retrieve computed results
- Dependency Injection: Automatically inject dependencies into class constructors
Common Mistakes
Forgetting to return what you need. Method, accessor, and class decorators can return values to replace the original. If you forget to return the descriptor (for methods/accessors) or constructor (for classes), you’ll lose the original behavior.
Decorating both getter and setter. TypeScript doesn’t allow decorating both accessors for the same property. Apply decorators to the first accessor in document order—the descriptor handles both get and set.
Assuming types change. Decorators that add properties at runtime don’t affect TypeScript’s type system. The compiler doesn’t know about runtime additions:
function addMetadata(target: Function) {
target.metadata = { created: new Date() };
}
@addMetadata
class Thing {}
const thing = new Thing();
// TypeScript error: Property 'metadata' does not exist
(thing as any).metadata; // Works at runtime
Conclusion
Decorators unlock powerful metaprogramming capabilities in TypeScript. They let you cleanly separate cross-cutting concerns like logging, validation, and caching from your business logic. Frameworks like Angular and NestJS demonstrate how decorators can create elegant, declarative APIs.
Start small—try adding a simple logging decorator to a method, then experiment with accessor decorators for validation. As you get comfortable, you’ll find decorators are one of TypeScript’s most versatile features.
Summary
| Decorator Type | Arguments | Use Case |
|---|---|---|
| Class | constructor | Modify or seal class, add metadata |
| Method | target, propertyKey, descriptor | Add logging, timing, make read-only |
| Accessor | target, propertyKey, descriptor | Validate values, transform on set |
| Property | target, propertyKey | Modify property behavior |
| Parameter | target, propertyKey, index | Mark required params, dependency injection |
Verified via: TypeScript Handbook - Decorators, TypeScript 5.0 Release Notes