TypeScript decorators let you attach behavior to a class, method, or property with a single @ symbol, and if you've used NestJS or Angular, you've been using them without necessarily knowing how they work.
TypeScript decorators are functions that run at class-definition time and can observe, modify, or replace the class, method, property, or parameter they're attached to. They're not unique to TypeScript — decorators are now a Stage 3 ECMAScript proposal, and TypeScript 5.0+ supports the modern spec-compliant version, which is different enough from the old experimentalDecorators behavior that it's worth understanding both.
Why Decorators Matter (and When to Skip Them)
Decorators exist to solve cross-cutting concerns cleanly — logging, validation, dependency injection, caching — without repeating the same wrapper logic in every method. Frameworks like NestJS lean on them heavily for exactly this reason: @Controller(), @Get(), and @Injectable() all use decorators to keep route definitions declarative.
Skip decorators if you're not already using a framework built around them. For plain application code, a decorator adds a layer of indirection that a regular higher-order function often handles more transparently.
Getting Started with Decorators
Enable them in tsconfig.json (for the modern spec-compliant version, no flag is needed in TypeScript 5.0+ with target: ES2022 or higher; for the legacy version you still need experimentalDecorators: true):
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": false
}
}
A minimal method decorator that logs every call:
function logCall(originalMethod: any, context: ClassMethodDecoratorContext) {
const methodName = String(context.name);
function replacementMethod(this: any, ...args: any[]) {
console.log(`Calling ${methodName} with`, args);
return originalMethod.call(this, ...args);
}
return replacementMethod;
}
class OrderService {
@logCall
createOrder(id: string) {
return { id, status: "created" };
}
}
Core Decorator Concepts Every Developer Should Know
Class decorators run once, when the class is defined, and can replace the constructor entirely — useful for things like automatically registering a class in a DI container.
Method decorators wrap the method implementation, exactly like the logCall example above. This is the most common decorator type in real codebases — validation, retries, and permission checks are all natural fits.
Property and accessor decorators intercept reads/writes to a field. NestJS's @Injectable() metadata and validation libraries like class-validator's @IsEmail() are built on this pattern.
Decorator factories. Most real decorators aren't bare functions — they're functions that return a decorator, so you can pass configuration:
function retry(times: number) {
return function (originalMethod: any, context: ClassMethodDecoratorContext) {
return async function (this: any, ...args: any[]) {
let lastError: unknown;
for (let i = 0; i < times; i++) {
try {
return await originalMethod.call(this, ...args);
} catch (err) {
lastError = err;
}
}
throw lastError;
};
};
}
class PaymentService {
@retry(3)
async chargeCard(amount: number) {
// ...
}
}
Common Decorator Mistakes and How to Fix Them
Mistake 1: mixing legacy and modern decorator syntax. The pre-5.0 experimentalDecorators API and the Stage 3 spec-compliant one have different signatures — a decorator written for one will silently misbehave under the other. Fix: check which mode your tsconfig.json uses before copying decorator code from older tutorials or Stack Overflow answers.
Mistake 2: decorators that swallow errors silently. A retry or logging decorator that catches an exception and doesn't rethrow makes debugging production issues nearly impossible, because the stack trace never surfaces. Fix: always rethrow after logging, or clearly document that a decorator suppresses errors.
Mistake 3: overusing decorators for simple logic. Wrapping a single-line function in a decorator for something that a plain if statement handles just as well adds cognitive overhead for no real benefit. Fix: reach for decorators when the same cross-cutting logic repeats across three or more methods, not for one-off cases.
When Should You Use Decorators?
Use them when you're already inside a framework that expects them (NestJS, TypeORM, Angular) or when the same wrapper logic — logging, auth checks, caching — repeats across many methods in a class-based codebase. Skip them in functional-style codebases where a higher-order function does the same job with less magic.
Decorators in Production
Metadata reflection (reflect-metadata) pairs with decorators in frameworks like NestJS to enable runtime type inspection for dependency injection — if you're building your own decorator-based system, that library is worth knowing about. Also test decorators in isolation: because they run at class-definition time, a bug in a decorator factory can break every class that uses it, so unit test the decorator function directly, not just the classes that consume it.
Before reaching for a decorator, check whether you're already in a framework that expects one — outside that context, a plain wrapper function is usually the simpler, more debuggable choice.