What a Decorator Is
A decorator is a function you attach to a class or one of its members using the @ symbol. When the class is defined, that function runs and is given the thing it was attached to, so it can inspect it, record it somewhere, or replace it with a wrapped version.
The purpose is to separate a concern from the code it applies to. Logging every call to a method, marking a class so a framework can find it later, timing an operation, checking permissions before a handler runs — all of these are the same few lines repeated in dozens of places. A decorator lets you write them once and apply them with one word.
This is why frameworks lean on them so heavily. Angular uses @Component and @Injectable to describe what a class is; NestJS uses @Controller and @Get to map classes and methods to HTTP routes; TypeORM uses @Entity and @Column to describe database tables. In each case the decorator is doing the same job: recording information about a class so the framework can wire things up at startup.
Decorators only work on classes and their members. There is no way to decorate a plain function or a standalone variable, which is one reason they are rare outside framework code.
// The shape of every decorator usage
@SomeDecorator
class Example {
@AnotherDecorator
method() {}
}
// What a framework looks like from the outside (NestJS style)
@Controller("students")
class StudentController {
@Get(":id")
findOne(id: string) {
return { id };
}
}
// Nothing above is magic. @Controller and @Get are just functions
// that were called when this class was defined, and that stored
// "this class handles /students" in a registry the framework reads later. - If you are learning TypeScript for React, Node scripts or general web work, decorators are the one topic in this course you can safely skim. They matter enormously in Angular, NestJS and TypeORM, and barely at all elsewhere.
There Are Two Decorator Systems — Know Which One You Are In
This is the most important thing to understand before writing a single decorator, and it is the reason so much decorator advice on the internet does not work when you paste it in.
TypeScript has supported decorators for years behind the experimentalDecorators compiler flag, based on an early version of the JavaScript proposal. That version is what Angular, NestJS and TypeORM are built on, and it is still fully supported. Separately, the JavaScript proposal was redesigned and reached a later stage, and TypeScript 5.0 shipped an implementation of that standard version, available with no flag at all.
The two systems use the same @ syntax and completely different function signatures. A decorator written for one will not work in the other. Which one you get depends on a single setting: if experimentalDecorators is on, you are using the legacy system; if it is off, you are using the standard one.
So the first thing to do in any project is open tsconfig.json and look. If you are working in Angular or NestJS, that flag will be on, and the legacy signatures are what you need. If you are starting something new with no framework requirement, leave it off and use the standard form.
experimentalDecorators: true— the legacy system; required by Angular, NestJS and TypeORM- Flag absent or false — the standard system introduced in TypeScript 5.0, no configuration needed
- The signatures are different: legacy decorators receive
(target, propertyKey, descriptor); standard ones receive(value, context) - Parameter decorators exist only in the legacy system — the standard proposal does not include them
emitDecoratorMetadata, which frameworks use for dependency injection, works only with the legacy system- You cannot mix the two in one project — the flag decides for every file
- When you find a decorator example online, check which system it is written for before debugging it. An error about the wrong number of arguments, or about a
contextobject that does not exist, almost always means the two systems have been mixed.
Standard Decorators (TypeScript 5.0 and later)
A standard decorator is a function that receives two arguments: the thing being decorated, and a context object describing it. The context tells you the kind of member ("class", "method", "getter", "field" and so on), its name, whether it is static or private, and gives you a way to register an initialiser.
For a method, the first argument is the original function and the context is typed ClassMethodDecoratorContext. If your decorator returns a function, that replacement is used in place of the original — which is how you wrap a method with logging, timing or a permission check. If it returns nothing, the original stays as it is and your decorator has simply observed it.
The generic parameters in the example below look heavy, and they are there for a reason: they preserve the method's own this type, argument types and return type, so a decorated method keeps its exact signature. You could type them all as any and the code would run, but every call site would lose its checking — the same bargain any always offers.
Note the small runtime detail in the replacement function: it uses a regular function, not an arrow, so that this is bound to the instance when the method is called.
// No compiler flag needed in TypeScript 5.0+
function logged<This, Args extends unknown[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
const name = String(context.name);
return function (this: This, ...args: Args): Return {
console.log(`-> ${name}(${args.join(", ")})`);
const result = target.call(this, ...args);
console.log(`<- ${name} returned ${String(result)}`);
return result;
};
}
class MarksService {
@logged
average(marks: number[]): number {
return marks.reduce((a, b) => a + b, 0) / marks.length;
}
}
new MarksService().average([88, 74, 91]);
// -> average(88,74,91)
// <- average returned 84.33333333333333 - The
contextobject also carriesaddInitializer, which schedules code to run when the class or instance is set up. That is how standard decorators register a class with a framework, replacing what the legacy system did with metadata.
Legacy Decorators, and Decorator Factories
If you are working in Angular, NestJS or TypeORM, this is the form you will read and write. Turn on experimentalDecorators and the signatures change: a class decorator receives the constructor function; a method decorator receives the prototype, the member name, and the property descriptor holding the actual function.
Replacing a method here means writing a new function onto descriptor.value, rather than returning a replacement. The pattern is the same idea in a different shape — keep a reference to the original, install a wrapper, and call the original with apply so that this and the arguments pass through untouched.
A decorator factory is the piece that makes framework decorators readable. @Component({ selector: "app-header" }) is not a decorator; it is a function call whose result is the decorator. Writing a factory means writing a function that takes your configuration and returns the real decorator function, which then closes over that configuration. Any decorator you see with parentheses is a factory.
One more detail that follows: decorator expressions are evaluated from top to bottom, but applied from the bottom up. With @A @B method(), B wraps the method first and A wraps the result — the same order as nested function calls.
// tsconfig.json: { "compilerOptions": { "experimentalDecorators": true } }
// Legacy class decorator: receives the constructor
function Registered(constructor: Function) {
console.log(`Registering ${constructor.name}`);
}
// Legacy method decorator: receives the descriptor
function Log(
target: unknown,
name: string,
descriptor: PropertyDescriptor
): void {
const original = descriptor.value;
descriptor.value = function (...args: unknown[]) {
console.log(`Calling ${name} with`, args);
return original.apply(this, args);
};
}
// A decorator FACTORY — note the parentheses at the usage site
function Route(path: string) {
return function (target: unknown, name: string, descriptor: PropertyDescriptor) {
console.log(`${path} -> ${name}`);
};
}
@Registered
class StudentService {
@Log
@Route("/students")
list() {
return ["Ananya", "Rahul"];
}
} - Frameworks that inject dependencies by type also need
emitDecoratorMetadata: trueand thereflect-metadatapackage. That combination is what lets Angular and NestJS read a constructor's parameter types at runtime — one of the very few places where type information survives compilation, and only because the compiler deliberately writes it out as data.
Decorators Emit Real Code — Use Them Sparingly
Almost everything else in this course disappears at compile time. Decorators do not. They are executable JavaScript that runs when your classes are defined, which puts them alongside enums as one of the few TypeScript features with a runtime footprint.
That has consequences worth weighing. Decorators run at definition time, before any of your application logic, so a decorator that throws breaks your program at import. They make the flow of control harder to follow, because a method's real behaviour may be defined somewhere the reader has to go and find. And a wrapped method is harder to test and to debug, since the stack trace passes through the wrapper.
None of that makes them bad. It makes them a tool with a cost, appropriate where the benefit is large and repeated — which is exactly the framework case. For your own application code, a plain higher-order function usually does the same job with less indirection and no compiler configuration.
The practical advice is simple: use the decorators your framework provides, learn to read them, and think twice before inventing your own.
- Use them when a framework requires them — that is what they are designed for
- Use them for cross-cutting concerns applied in many places: logging, timing, authorisation checks, registration
- Prefer a plain wrapper function when there is only one call site
- Avoid decorators that change a method's signature or return type; callers cannot see the change
- Keep decorators free of heavy work — they run at import time, before your program starts
- Check which decorator system your project uses before copying any example
- The standard decorator proposal is still settling, and details such as decorator metadata continue to evolve. Before building anything substantial on standard decorators, check the TypeScript release notes for the version you are actually using.
