TypeScript Decorators
A decorator is a special function you attach to a class, a method, a property or an accessor with the @name syntax. It runs when the class is defined and can add metadata to, change, or replace the thing it decorates.
Decorators are used heavily in frameworks like Angular and NestJS for dependency injection, routing and more.
Enabling Decorators
Modern TypeScript (5.0 and later) supports the standard (TC39) decorator syntax out of the box — no compiler option is needed, and that is the form used by every example on this page.
Older code uses the legacy decorator design. To compile that form you must switch it on in tsconfig.json:
"compilerOptions": {"target": "ES2020","module": "commonjs","experimentalDecorators": true,"emitDecoratorMetadata": true,"strictPropertyInitialization": false},"include": ["src/**/*.ts"]
Note: emitDecoratorMetadata makes the compiler emit type information for legacy decorators. Libraries like TypeORM and class-validator rely on it.
Types of Decorators
TypeScript lets you decorate several kinds of declarations. With standard decorators every decorator receives two arguments: the value being decorated and a context object that tells you its kind and name.
- Class decorator — applies to class declarations:
(constructor: Function, context: ClassDecoratorContext) => void | Function - Method decorator — applies to class methods:
(method: Function, context: ClassMethodDecoratorContext) => void | Function - Accessor decorator — applies to
accessorfields (the standard way to intercept reads and writes of a property):(target: { get, set }, context: ClassAccessorDecoratorContext) => { get?, set?, init? } - Field decorator — applies to plain class properties:
(value: undefined, context: ClassFieldDecoratorContext) => void | ((initialValue) => any)
The legacy design instead passes (target, propertyKey, descriptor) to method decorators, (target, propertyKey) to property decorators and (target, propertyKey, parameterIndex) to parameter decorators. Parameter decorators exist only in the legacy design.
Class Decorators
A class decorator receives the class constructor. You can use it to observe the class, change it, or even return a new constructor that replaces it.
It is called once, when the class is declared — not each time you create an instance.
Basic Class Decorator
This simple decorator logs a message the moment the class is defined.
logClass decorator that takes the class constructor and a ClassDecoratorContext and logs Class <name> was defined at <ISO timestamp>. Apply it with @logClass to a UserService class that has a getUsers() method returning ["Alice", "Bob", "Charlie"]. Nothing else is called — the log line appears as soon as the file loads.
Output: TypeScript file and captured output
// A simple class decorator that logs class definition
function logClass(constructor: Function, context: ClassDecoratorContext) {
console.log(`Class ${constructor.name} was defined at ${new Date().toISOString()}`);
}
// Applying the decorator
@logClass
class UserService {
getUsers() {
return ['Alice', 'Bob', 'Charlie'];
}
}
// Output when the file is loaded: "Class UserService was defined at [timestamp]"
Class Decorator with Constructor Modification
This example modifies a class: it adds a version property and returns a wrapper constructor that logs every time an instance is created.
versioned(version: string) returning a class decorator that sets prototype.version, then returns a wrapper constructor that logs Creating instance of <name> v<version> and calls the original with new, copying the prototype so instanceof keeps working. Apply @versioned("1.0.0") to an ApiClient class with a fetchData() method that logs Fetching data...; create an instance, print its version (cast to any) and call fetchData().
Output: TypeScript file and captured output
// A decorator that adds a version property and logs instantiation
function versioned(version: string) {
return function (constructor: Function, context: ClassDecoratorContext) {
// Add a static property
constructor.prototype.version = version;
// Store the original constructor
const original: any = constructor;
// Create a new constructor that wraps the original
const newConstructor: any = function (...args: any[]) {
console.log(`Creating instance of ${original.name} v${version}`);
return new original(...args);
};
// Copy prototype so instanceof works
newConstructor.prototype = original.prototype;
return newConstructor;
};
}
// Applying the decorator with a version
@versioned('1.0.0')
class ApiClient {
fetchData() {
console.log('Fetching data...');
}
}
const client = new ApiClient();
console.log((client as any).version); // Outputs: 1.0.0
client.fetchData();
Sealed Class Decorator
This decorator stops new properties from being added to the class and marks the existing ones as non-configurable.
sealed decorator that logs Sealing <name>... and calls Object.seal on both the constructor and its prototype. Apply it with @sealed to a Greeter class holding a greeting: string set in the constructor and a greet() method returning Hello, <greeting>. Leave a commented-out line showing that adding Greeter.prototype.newMethod would fail.
Output: TypeScript file and captured output
function sealed(constructor: Function, context: ClassDecoratorContext) {
console.log(`Sealing ${constructor.name}...`);
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return `Hello, ${this.greeting}`;
}
}
// This will throw an error in strict mode
// Greeter.prototype.newMethod = function() {}; // Error: Cannot add property newMethod
Method Decorators
A method decorator is placed on a method and can observe, change or replace it.
It receives two parameters:
originalMethod: the method function itselfcontext: aClassMethodDecoratorContextwith the method'sname, whether it isstaticorprivate, and anaddInitializerhook
If the decorator returns a function, that function replaces the method. Wrapping the original this way is how you add logging, timing or checks around a call.
Key points about method decorators:
- They run when the method is defined, not when it is called
- They can change the method's behavior by wrapping it with extra logic
- They are used for cross-cutting concerns like logging, validation and authorization
- They must return a new function or
undefined(when nothing is replaced)
Method Timing Decorator
This decorator measures and logs how long a method takes to execute.
measureTime(originalMethod, context: ClassMethodDecoratorContext) returning a wrapper that records performance.now() before and after calling the original and logs <method> executed in <ms>ms with two decimals. Apply it to processData(data: number[]): number[] on a DataProcessor class that loops 100,000,000 times to simulate work and returns each value doubled; call it with [1, 2, 3, 4, 5].
Output: TypeScript file and captured output
// Method decorator to measure execution time
function measureTime(
originalMethod: any,
context: ClassMethodDecoratorContext
) {
const propertyKey = String(context.name);
return function (this: any, ...args: any[]) {
const start = performance.now();
const result = originalMethod.apply(this, args);
const end = performance.now();
console.log(`${propertyKey} executed in ${(end - start).toFixed(2)}ms`);
return result;
};
}
// Using the decorator
class DataProcessor {
@measureTime
processData(data: number[]): number[] {
// Simulate processing time
for (let i = 0; i < 100000000; i++) { /* processing */ }
return data.map(x => x * 2);
}
}
// When called, it will log the execution time
const processor = new DataProcessor();
processor.processData([1, 2, 3, 4, 5]);
Method Authorization Decorator
This example implements role-based access control: the decorator checks the current user's roles before letting the method run.
type UserRole = "admin" | "editor" | "viewer" and a currentUser object (id 1, name John Doe, roles ["viewer"]). Write AllowedRoles(...roles) returning a method decorator whose wrapper throws User <name> is not authorized to call <method> when the current user has none of the roles. In a DocumentService, allow deleteDocument for admin/editor and viewDocument for all three; inside a try/catch call view then delete as a viewer (the second throws and its message is printed with console.error), then switch roles to ["admin"] and delete again successfully.
Output: TypeScript file and captured output
// User roles
type UserRole = 'admin' | 'editor' | 'viewer';
// Current user context (simplified)
const currentUser = {
id: 1,
name: 'John Doe',
roles: ['viewer'] as UserRole[]
};
// Decorator factory for role-based access control
function AllowedRoles(...allowedRoles: UserRole[]) {
return function (
originalMethod: any,
context: ClassMethodDecoratorContext
) {
const propertyKey = String(context.name);
return function (this: any, ...args: any[]) {
const hasPermission = allowedRoles.some(role =>
currentUser.roles.includes(role)
);
if (!hasPermission) {
throw new Error(
`User ${currentUser.name} is not authorized to call ${propertyKey}`
);
}
return originalMethod.apply(this, args);
};
};
}
// Using the decorator
class DocumentService {
@AllowedRoles('admin', 'editor')
deleteDocument(id: string) {
console.log(`Document ${id} deleted`);
}
@AllowedRoles('admin', 'editor', 'viewer')
viewDocument(id: string) {
console.log(`Viewing document ${id}`);
}
}
// Usage
const docService = new DocumentService();
try {
docService.viewDocument('doc123'); // Works - viewer role is allowed
docService.deleteDocument('doc123'); // Throws error - viewer cannot delete
} catch (error) {
console.error(error.message);
}
// Change user role to admin
currentUser.roles = ['admin'];
docService.deleteDocument('doc123'); // Now works - admin can delete
Deprecation Warning Decorator
This decorator prints a deprecation warning whenever a method that will be removed in a future version is called.
deprecated(message: string) returning a method decorator whose wrapper calls console.warn with Warning: <method> is deprecated. <message> and then the original method. In a PaymentService mark processPayment(amount, currency) with @deprecated("Use processPaymentV2 instead"), keep processPaymentV2 undecorated, and call both with 100, "USD".
Output: TypeScript file and captured output
function deprecated(message: string) {
return function (
originalMethod: any,
context: ClassMethodDecoratorContext
) {
const propertyKey = String(context.name);
return function (this: any, ...args: any[]) {
console.warn(`Warning: ${propertyKey} is deprecated. ${message}`);
return originalMethod.apply(this, args);
};
};
}
class PaymentService {
@deprecated('Use processPaymentV2 instead')
processPayment(amount: number, currency: string) {
console.log(`Processing payment of ${amount} ${currency}`);
}
processPaymentV2(amount: number, currency: string) {
console.log(`Processing payment v2 of ${amount} ${currency}`);
}
}
const payment = new PaymentService();
payment.processPayment(100, 'USD'); // Shows deprecation warning
payment.processPaymentV2(100, 'USD'); // No warning
Property Decorators
Property decorators let you observe or change how a property is read and written.
With standard decorators the property is declared with the accessor keyword. The decorator then receives two parameters:
target: an object with the generatedgetandsetfunctionscontext: aClassAccessorDecoratorContextwith the property'sname
Returning an object with your own get and set replaces the default accessors.
Key points about property decorators:
- They run when the class is defined, not when the property is accessed
- A decorator on a plain (non-
accessor) field can only adjust the initial value; to intercept reads and writes useaccessor - In the legacy design they receive
(target, propertyKey)and must useObject.definePropertyto change behavior - They are often used for formatting, logging, validation or metadata
- They can be combined with other decorators for more complex behavior
Formatted Property Decorator
This decorator automatically formats a property every time it is set.
format(formatString: string) returning an accessor decorator that keeps a private value and returns a get/set pair where the setter stores formatString with {} replaced by the new value. Apply @format("Hello, {}!") to accessor greeting: string in a Greeter class, set greeting to "World" and print it (expected Hello, World!).
Output: TypeScript file and captured output
// Property decorator to format a string property
function format(formatString: string) {
return function (target: ClassAccessorDecoratorTarget<any, string>, context: ClassAccessorDecoratorContext) {
let value: string;
const getter = () => value;
const setter = (newVal: string) => {
value = formatString.replace('{}', newVal);
};
return {
get: getter,
set: setter
};
};
}
class Greeter {
@format('Hello, {}!')
accessor greeting: string;
}
const greeter = new Greeter();
greeter.greeting = 'World';
console.log(greeter.greeting); // Outputs: Hello, World!
Logging Property Decorator
This decorator logs every read and every change of a property.
logProperty that returns a getter logging Getting <name>: <value> and a setter logging Setting <name> from <old> to <new>. Apply it to accessor name: string and accessor price: number of a Product class whose constructor assigns both. Create new Product("Laptop", 999.99), set price to 899.99, then print product.name.
Output: TypeScript file and captured output
function logProperty(target: ClassAccessorDecoratorTarget<any, any>, context: ClassAccessorDecoratorContext) {
const propertyKey = String(context.name);
let value: any;
const getter = function() {
console.log(`Getting ${propertyKey}: ${value}`);
return value;
};
const setter = function(newVal: any) {
console.log(`Setting ${propertyKey} from ${value} to ${newVal}`);
value = newVal;
};
return {
get: getter,
set: setter
};
}
class Product {
@logProperty
accessor name: string;
@logProperty
accessor price: number;
constructor(name: string, price: number) {
this.name = name;
this.price = price;
}
}
const product = new Product('Laptop', 999.99);
product.price = 899.99; // Logs: Setting price from 999.99 to 899.99
console.log(product.name); // Logs: Getting name: Laptop
Required Property Decorator
This decorator makes sure a property has been given a value before it is read.
required whose getter throws Property <name> is required when the stored value is undefined and whose setter simply stores the value. Apply it to accessor username: string and accessor email: string of a User class (with an optional age?: number) whose constructor sets both. Create new User("johndoe", "[email protected]"), which works, and leave commented-out lines showing that passing undefined would throw. The program prints nothing.
Output: TypeScript file and captured output
function required(target: ClassAccessorDecoratorTarget<any, any>, context: ClassAccessorDecoratorContext) {
const propertyKey = String(context.name);
let value: any;
const getter = function() {
if (value === undefined) {
throw new Error(`Property ${propertyKey} is required`);
}
return value;
};
const setter = function(newVal: any) {
value = newVal;
};
return {
get: getter,
set: setter
};
}
class User {
@required
accessor username: string;
@required
accessor email: string;
age?: number;
constructor(username: string, email: string) {
this.username = username;
this.email = email;
}
}
const user1 = new User('johndoe', '[email protected]'); // Works
// const user2 = new User(undefined, '[email protected]'); // Throws error: Property username is required
// const user2 = new User('johndoe', undefined); // Throws error: Property email is required
Parameter Decorators
In the legacy decorator design you can also decorate a single parameter of a constructor or method. Such a decorator receives three parameters:
target: the prototype of the class (for instance methods) or the constructor (for static methods)propertyKey: the name of the method (orundefinedfor constructor parameters)parameterIndex: the position of the parameter in the parameter list
Standard decorators do not have parameter decorators. The example below keeps the idea — describe a rule per parameter, then check the arguments when the method is called — but passes the rules to a method decorator in parameter order, so it compiles with default settings.
Key points about parameter decorators:
- They run when the method is defined, not when it is called
- They are usually paired with a method decorator that does the real work
- With the
reflect-metadatalibrary they can store and read metadata - They are common in dependency injection frameworks
- The parameter index lets you find the matching argument at runtime
Parameter Validation Decorator
This example validates method parameters: each rule says which type is expected at which position, and the method decorator checks the arguments on every call.
validateParam(type) that returns a rule object for "string" | "number" | "boolean", and a validate(...rules) method decorator that maps the rules to { index, type } entries and, on every call, checks each argument (non-empty string, non-NaN number, boolean) and throws Parameter at index <i> failed <type> validation on failure. Apply @validate(validateParam("string"), validateParam("number"), validateParam("boolean")) to createUser(name, age, isActive) on a UserService that logs Creating user: <name>, <age>, <isActive>; call it with "John", 30, true and leave commented-out failing calls.
Output: TypeScript file and captured output
type ParamType = 'string' | 'number' | 'boolean';
// Describes one parameter rule: which type is expected at which position
function validateParam(type: ParamType) {
return { type };
}
// Method decorator: the rules are listed in the same order as the parameters
function validate(...rules: { type: ParamType }[]) {
return function (originalMethod: any, context: ClassMethodDecoratorContext) {
const validations: Array<{index: number, type: string}> =
rules.map((rule, index) => ({ index, type: rule.type }));
return function (this: any, ...args: any[]) {
for (const validation of validations) {
const { index, type } = validation;
const param = args[index];
let isValid = false;
switch (type) {
case 'string':
isValid = typeof param === 'string' && param.length > 0;
break;
case 'number':
isValid = typeof param === 'number' && !isNaN(param);
break;
case 'boolean':
isValid = typeof param === 'boolean';
}
if (!isValid) {
throw new Error(`Parameter at index ${index} failed ${type} validation`);
}
}
return originalMethod.apply(this, args);
};
};
}
class UserService {
@validate(validateParam('string'), validateParam('number'), validateParam('boolean'))
createUser(name: string, age: number, isActive: boolean) {
console.log(`Creating user: ${name}, ${age}, ${isActive}`);
}
}
const service = new UserService();
service.createUser('John', 30, true); // Works
// service.createUser('', 30, true); // Throws error: Parameter at index 0 failed string validation
// service.createUser('John', 'thirty' as any, true); // Throws error: Parameter at index 1 failed number validation
Decorator Factories
A decorator factory is a function that returns a decorator.
Because you call the factory with arguments, you can configure the decorator it produces. This makes decorators more flexible and reusable.
Configurable Logging Decorator
This example builds a logging decorator that can be customized with a log level and a message.
logWithConfig(config: { level: "log" | "warn" | "error", message?: string }) returning a method decorator whose wrapper prints <message>: <method> plus the arguments through console[level], runs the original method, then prints <method> completed. Apply it with level log and message Processing payment to processPayment(amount: number) in a PaymentService that logs Processing payment of $<amount>. Only declare the class — nothing is called, so there is no output.
Output: TypeScript file and captured output
// Decorator factory that accepts configuration
function logWithConfig(config: { level: 'log' | 'warn' | 'error', message?: string }) {
return function (originalMethod: any, context: ClassMethodDecoratorContext) {
const propertyKey = String(context.name);
return function (this: any, ...args: any[]) {
const { level = 'log', message = 'Executing method' } = config;
console[level](`${message}: ${propertyKey}`, { arguments: args });
const result = originalMethod.apply(this, args);
console[level](`${propertyKey} completed`);
return result;
};
};
}
class PaymentService {
@logWithConfig({ level: 'log', message: 'Processing payment' })
processPayment(amount: number) {
console.log(`Processing payment of $${amount}`);
}
}
Order of Evaluation
When several decorators are stacked on one declaration, the factories are evaluated top to bottom, and then the decorators they returned are called bottom to top.
first() and second() that each log <name>(): factory evaluated and return a method decorator that logs <name>(): called. Stack @first() above @second() on an empty method() in ExampleClass. Expected order: both factories top to bottom, then the decorators bottom to top (second(): called before first(): called).
Output: TypeScript file and captured output
function first() {
console.log('first(): factory evaluated');
return function (originalMethod: any, context: ClassMethodDecoratorContext) {
console.log('first(): called');
};
}
function second() {
console.log('second(): factory evaluated');
return function (originalMethod: any, context: ClassMethodDecoratorContext) {
console.log('second(): called');
};
}
class ExampleClass {
@first()
@second()
method() {}
}
// Output:
// first(): factory evaluated
// second(): factory evaluated
// second(): called
// first(): called
Real-World Examples
API Controller with Decorators
This example uses decorators to describe a small API controller, similar to what NestJS or Express-based frameworks do.
ROUTES array. Write a Controller(prefix) class decorator that stores the prefix on the prototype and assigns the class to routes collected so far, and a Get(path) method decorator that pushes { method: "get", path, handler }. Decorate a UserController with @Controller("/users") and two methods @Get("/") getAllUsers() and @Get("/:id") getUserById(id). A registerRoutes() function logs Registered GET <prefix><path> for every route.
Output: TypeScript file and captured output
// Simple decorator implementations (simplified for example)
const ROUTES: any[] = [];
function Controller(prefix: string = '') {
return function (constructor: Function, context: ClassDecoratorContext) {
constructor.prototype.prefix = prefix;
// Method decorators run before the class decorator, so the routes
// collected so far without a target belong to this class
ROUTES.forEach(route => {
if (!route.target) {
route.target = constructor;
}
});
};
}
function Get(path: string = '') {
return function (originalMethod: any, context: ClassMethodDecoratorContext) {
ROUTES.push({
method: 'get',
path,
handler: originalMethod,
target: undefined
});
};
}
// Using the decorators
@Controller('/users')
class UserController {
@Get('/')
getAllUsers() {
return { users: [{ id: 1, name: 'John' }] };
}
@Get('/:id')
getUserById(id: string) {
return { id, name: 'John' };
}
}
// Simulate route registration
function registerRoutes() {
ROUTES.forEach(route => {
const prefix = route.target.prototype.prefix || '';
console.log(`Registered ${route.method.toUpperCase()} ${prefix}${route.path}`);
});
}
registerRoutes();
// Output:
// Registered GET /users/
// Registered GET /users/:id
Best Practices
Follow these practices when you write decorators:
- Keep decorators focused: each decorator should do one thing.
- Document behavior: say clearly what a decorator does and which side effects it has.
- Use decorator factories: make decorators configurable so they can be reused.
- Consider performance: a decorator that wraps a method adds work to every call.
- Type safety: use the context types (
ClassMethodDecoratorContextand friends) to keep decorators type-safe. - Error handling: handle errors properly inside decorators.
- Testing: write unit tests for your decorators.
- Metadata: use
reflect-metadatafor advanced cases that need runtime type information.
Common Pitfalls
Avoid these common mistakes:
- Wrong decorator flavor: legacy-style decorators need
experimentalDecoratorsintsconfig.json; standard decorators must not have it. - Incorrect decorator signature: each decorator kind has its own parameters. The wrong ones lead to runtime errors.
- Evaluation order: stacked decorators are applied bottom to top.
- Property initialization: property decorators run before instance properties are initialized.
- Metadata reflection: forgetting to import
reflect-metadatawhen you use decorator metadata. - Performance overhead: be careful with heavy decorators in performance-critical code.
- Browser compatibility: decorators are a stage 3 proposal, so older browsers need transpiled output.
The two snippets below use the legacy syntax. To compile them, add this to tsconfig.json: "compilerOptions": { "experimentalDecorators": true }.
Where are decorators used?
- Angular: components, services, modules and more
- NestJS: controllers, providers, routes and more
Legacy readonly property decorator
A legacy-style property decorator that uses Object.defineProperty to make name non-writable.
function readonly(target: any, propertyKey: string) {
Object.defineProperty(target, propertyKey, {
writable: false
});
}
class Person {
@readonly
name = "John";
}
Legacy parameter decorator
A legacy-style parameter decorator that logs the method name and the index of the decorated parameter.
function logParameter(target: any, propertyKey: string, parameterIndex: number) {
console.log(`Parameter in ${propertyKey} at index ${parameterIndex}`);
}
class Demo {
greet(@logParameter message: string) {
return message;
}
}