TypeScript Decorators — Complete Guide
In this tutorial, you will learn about TypeScript Decorators. We cover key concepts, practical examples, and best practices to help you master this topic.
TypeScript decorators are special declarations that attach to classes, methods, properties, accessors, and parameters to modify their behavior at design time — enabling metaprogramming patterns like logging, validation, authorization, and dependency injection.
What You'll Learn
- Class decorators for extending constructors
- Method decorators for wrapping behavior
- Property and accessor decorators
- Parameter decorators
- Metadata reflection with
reflect-metadata
Why It Matters
Decorators are the foundation of Angular and NestJS — two of the most popular TypeScript frameworks. Understanding decorators unlocks the ability to write cross-cutting concerns (logging, caching, validation) as reusable annotations rather than repetitive boilerplate.
Real-World Use
NestJS (used by DodaTech for internal microservices) uses decorators extensively: @Controller(), @Get(), @Body(), @Injectable(). Each decorator adds metadata that NestJS's runtime uses to wire up routing, dependency injection, and request handling.
Learning Path
flowchart LR A[Inheritance] --> B[Decorators] B --> C[This Typing] B --> D[You Are Here] C --> E[Index Signatures] E --> F[Type Guards]
Enabling Decorators
Decorators are an experimental feature in TypeScript. Enable in tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Class Decorators
A class decorator receives the constructor and can modify or replace it:
function LogClass<T extends { new (...args: any[]): object }>(target: T): T {
console.log(`Class defined: ${target.name}`);
return target;
}
@LogClass
class UserService {
constructor() {
console.log("UserService instance created");
}
getUsers(): string[] {
return ["Alice", "Bob"];
}
}
// Output when class is defined:
// Class defined: UserService
const service = new UserService();
// UserService instance created
Modifying the Constructor
function Singleton<T extends { new (...args: any[]): object }>(target: T): T {
let instance: T;
const modified = class extends target {
constructor(...args: any[]) {
super(...args);
if (instance) {
return instance;
}
instance = this as unknown as T;
}
} as unknown as T;
return modified;
}
@Singleton
class DatabaseConnection {
connect(): void {
console.log("Connected to database");
}
}
const db1 = new DatabaseConnection();
const db2 = new DatabaseConnection();
console.log(db1 === db2); // true — same instance
Method Decorators
A method decorator intercepts property descriptor and can wrap the original method:
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor): void {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`[LOG] Calling ${propertyKey} with args:`, args);
const result = originalMethod.apply(this, args);
console.log(`[LOG] ${propertyKey} returned:`, result);
return result;
};
}
class Calculator {
@Log
add(a: number, b: number): number {
return a + b;
}
@Log
multiply(a: number, b: number): number {
return a * b;
}
}
const calc = new Calculator();
calc.add(3, 4); // [LOG] Calling add with args: [3, 4]
// [LOG] add returned: 7
calc.multiply(2, 5); // [LOG] Calling multiply...
Async Method Decorator
function Time(target: any, propertyKey: string, descriptor: PropertyDescriptor): void {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
const start = Date.now();
const result = await originalMethod.apply(this, args);
const duration = Date.now() - start;
console.log(`[PERF] ${propertyKey} took ${duration}ms`);
return result;
};
}
class ScanService {
@Time
async scanFile(path: string): Promise<string> {
// Simulate async scan
await new Promise(resolve => setTimeout(resolve, 100));
return `Scan of ${path} complete`;
}
}
Authorization Decorator
function Authorized(role: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): void {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
// In real app, get user from request context
const userRole = "admin";
if (userRole !== role && userRole !== "admin") {
throw new Error(`Unauthorized: ${role} role required`);
}
return originalMethod.apply(this, args);
};
};
}
class AdminAPI {
@Authorized("admin")
deleteUser(userId: string): void {
console.log(`Deleted user ${userId}`);
}
@Authorized("user")
viewProfile(userId: string): void {
console.log(`Viewing profile ${userId}`);
}
}
Property Decorators
function Format(formatStr: string) {
return function (target: any, propertyKey: string): void {
let value: string;
const getter = function () {
return value;
};
const setter = function (newVal: string) {
value = formatStr.replace("%s", newVal);
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
};
}
class Greeting {
@Format("Hello, %s!")
message!: string;
}
const greet = new Greeting();
greet.message = "Alice";
console.log(greet.message); // Hello, Alice!
Parameter Decorators
import "reflect-metadata";
function Inject(serviceName: string): ParameterDecorator {
return (target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) => {
const existingParams: string[] = Reflect.getOwnMetadata("inject:params", target, propertyKey!) || [];
existingParams[parameterIndex] = serviceName;
Reflect.defineMetadata("inject:params", existingParams, target, propertyKey!);
};
}
class UserController {
private userService: any;
constructor(@Inject("UserService") userService: any) {
this.userService = userService;
const params: string[] = Reflect.getOwnMetadata("inject:params", this, "constructor");
console.log("Injected:", params); // Injected: ["UserService"]
}
}
new UserController({});
Decorator Factories
A decorator factory is a function that returns a decorator, allowing parameterization:
function Log(prefix: string = "LOG") {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): void {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`[${prefix}] ${propertyKey} called`);
return originalMethod.apply(this, args);
};
};
}
class Service {
@Log("TRACE")
doSomething(): void {
console.log("Doing something");
}
@Log("PERF")
doSomethingElse(): void {
console.log("Doing something else");
}
}
Common Mistakes
1. Forgetting to Enable experimentalDecorators
Without this flag, decorators cause compile errors.
2. Using Decorators in Runtime-Heavy Ways
Decorators execute when the class is defined (not instantiated). Avoid heavy computation in decorator factories.
3. Not Handling this Context in Method Decorators
Use function() (not arrow function) in the wrapped method to preserve this:
// Arrow function — loses this context
descriptor.value = (...args: any[]) => originalMethod.apply(this, args);
4. Decorator Order Confusion
Decorators apply in reverse order of their proximity to the target:
@DecoratorA
@DecoratorB
class MyClass {} // DecoratorB runs on the class, then DecoratorA
5. Relying on Decorators for Runtime Type Safety
Decorators can enhance type safety but don't replace proper TypeScript types.
Practice Questions
What is a decorator factory? A function that returns a decorator, allowing configuration via parameters.
What can decorators be applied to? Classes, methods, properties, accessors (get/set), and parameters.
What is the purpose of
emitDecoratorMetadata? It enables TypeScript to emit type metadata that can be read at runtime viareflect-metadata.How do method decorators work? They receive the target Prototype, method name, and property descriptor, allowing them to wrap or replace the original method.
Challenge: Write a @Throttle(ms: number) method decorator that prevents a method from being called more than once every ms milliseconds. Use it on a handleClick() method.
FAQ
Mini Project: Middleware System
// src/middleware.ts
function Middleware(...fns: Array<(ctx: any, next: () => void) => void>) {
return function <T extends { new (...args: any[]): object }>(target: T): T {
return class extends target {
constructor(...args: any[]) {
super(...args);
}
executeMethod(method: string, ...methodArgs: any[]): any {
let index = 0;
const runMiddleware = () => {
if (index < fns.length) {
const fn = fns[index++];
fn({ method, args: methodArgs }, runMiddleware);
}
};
runMiddleware();
return (this as any)[method](...methodArgs);
}
} as unknown as T;
};
}
function Logging(ctx: any, next: () => void): void {
console.log(`[Middleware] ${ctx.method} called with:`, ctx.args);
next();
}
function Validation(ctx: any, next: () => void): void {
if (ctx.method === "createUser" && (!ctx.args[0]?.name)) {
throw new Error("Validation failed: name is required");
}
next();
}
@Middleware(Logging, Validation)
class UserAPI {
createUser(data: { name: string }): string {
console.log(`Creating user: ${data.name}`);
return `User ${data.name} created`;
}
deleteUser(id: string): string {
console.log(`Deleting user: ${id}`);
return `User ${id} deleted`;
}
}
const api = new UserAPI();
console.log(api.createUser({ name: "Alice" }));
// [Middleware] createUser called with: [{ name: "Alice" }]
// Creating user: Alice
// User Alice created
What's Next
Now explore this typing for safer context binding:
| Lesson | Description |
|---|---|
| {{< ref "/programming-languages/typescript/21-inheritance" >}} | Review inheritance |
| {{< ref "/programming-languages/typescript/23-this-typing" >}} | This parameter and type |
| {{< ref "/programming-languages/typescript/24-index-signatures" >}} | Index signatures and Record utility |
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro