Decorators in TypeScript Explained
TypeScript decorators are functions that modify classes, methods, properties, and parameters at design time. Learn the different decorator types and how to use them.
A TypeScript decorator is a special function that you attach to a class, method, or field using the @ symbol. Decorators run when the class is defined (not when instances are created), and they let you observe, modify, or replace the thing they are attached to.
Since TypeScript 5.0, decorators follow the native TC39 stage 3 proposal and work without any compiler flag. This article covers that native syntax, since it is what new TypeScript code should use.
Older codebases and some frameworks still use the earlier experimentalDecorators mode, which has a different function signature and requires this tsconfig setting:
{
"compilerOptions": {
"experimentalDecorators": true
}
}The two decorator systems are not interchangeable: a decorator function written for experimentalDecorators will not work as a native decorator, and vice versa. If you are starting a new project, prefer the native decorators shown below.
Class Decorators
A class decorator receives the class itself and a context object, and it can observe the class or return a completely new class to replace it. The context object's name property gives you the class name without relying on the constructor's own name property.
Here is a simple class decorator that logs when a class is defined:
function reportable(value: Function, context: ClassDecoratorContext) {
console.log(`Class ${context.name} was defined.`);
}
@reportable
class User {
constructor(public name: string) {}
}When this file loads, the decorator runs immediately and prints to the console. The decorator does not change the class here. It only observes it.
A class decorator can also replace the class entirely. This is useful for adding default properties or wrapping the constructor logic:
function withTimestamp<T extends new (...args: any[]) => object>(
value: T,
context: ClassDecoratorContext
) {
return class extends value {
createdAt = new Date().toISOString();
};
}The decorator returns a new class that extends the original one, adding a createdAt field. Let us apply it to a class and create an instance:
@withTimestamp
class Order {
constructor(public item: string) {}
}
const order = new Order("Book");
console.log(order.item);The decorator adds a createdAt field to every Order instance at runtime. However, TypeScript does not know about this field, so accessing order.createdAt directly in TypeScript code would produce a type error unless you also declare it on the class.
Method Decorators
A method decorator receives the original method and a context object, and it runs once per method at class definition time. Returning a new function replaces the method with that function.
Here is a method decorator that wraps a method with logging:
function loggedMethod(originalMethod: any, context: ClassMethodDecoratorContext) {
const methodName = String(context.name);
return function (this: any, ...args: any[]) {
console.log(`Entering ${methodName}`);
const result = originalMethod.call(this, ...args);
console.log(`Exiting ${methodName}`);
return result;
};
}The replacement function calls the original method with originalMethod.call(this, ...args), then logs before and after. Now apply it to a method:
class Greeter {
constructor(private greeting: string) {}
@loggedMethod
greet() {
return "Hello, " + this.greeting;
}
}
new Greeter("world").greet();This prints Entering greet, then Exiting greet, around the method's own return value, since loggedMethod wraps the original greet call.
Entering greet
Exiting greetTo pass configuration into a decorator, write a decorator factory: a function that returns the actual decorator. @loggedMethod("LOG:") would call a factory that closes over the "LOG:" prefix and returns a function shaped like the one above.
Field Decorators and Metadata
A field decorator receives undefined as its first argument, since fields have no value yet at decoration time, and a context object as its second. Since TypeScript 5.2, that context object exposes a metadata property, backed by Symbol.metadata, which decorators can use to attach data to the class:
function format(formatString: string) {
return function (value: undefined, context: ClassFieldDecoratorContext) {
context.metadata[context.name] = formatString;
};
}
class User {
@format("ID: %s")
id: string = "usr_123";
}
console.log(User[Symbol.metadata]);This prints an object containing id: "ID: %s", since the decorator wrote the format string into the shared metadata object under the field's name.
Using context.metadata requires a compile target of es2022 or lower, with esnext (or esnext.decorators) added to the lib setting. This is different from legacy decorators, which store metadata through the separate reflect-metadata library and the emitDecoratorMetadata compiler option; that combination does not work with native decorators.
When Multiple Decorators Stack
You can apply multiple decorators to the same declaration. They evaluate top-to-bottom for the factory calls and bottom-to-top for the returned decorator functions:
function first() {
console.log("first: factory");
return (t: any, c: ClassMethodDecoratorContext) => { console.log("first: decorator"); return t; };
}
function second() {
console.log("second: factory");
return (t: any, c: ClassMethodDecoratorContext) => { console.log("second: decorator"); return t; };
}
class Example {
@first()
@second()
method() {}
}This prints the four lines below, in an order that reveals two separate passes: the factory calls run top to bottom, then the returned decorator functions run bottom to top.
first: factory
second: factory
second: decorator
first: decoratorThe factories run in the order they appear (top to bottom). The decorator functions run in reverse order (bottom to top), similar to function composition in mathematics where applying f after g is written f(g(x)).
When to Use Decorators
Decorators are most useful when:
- You want to attach cross-cutting behavior (logging, timing, validation) to many methods without repeating the code.
- You are building a framework that needs to discover and register classes or methods at startup.
- You are using an older framework version that relies on legacy decorators and experimentalDecorators for configuration, such as many Angular and NestJS dependency injection setups.
Do not use decorators for simple one-off behavior that could be a plain function call, or when the decorator logic is so simple that the @ syntax adds more confusion than value.
Common Mistakes
Mixing native and legacy decorator code. A decorator function written with the (target, key, descriptor) signature will not work as a native (value, context) decorator. Check which mode a library or example targets before copying its code.
Expecting TypeScript to know about decorator-added properties. TypeScript does not update the class type based on what a decorator adds. If a decorator adds a field, declare it on the class explicitly or use declaration merging.
Using decorators for logic that should run per-instance. Decorators run once at class definition time. If you need logic that runs for every new instance, use context.addInitializer or put the logic in the constructor instead.
For more on class features and patterns, see classes in TypeScript explained and parameter properties in TypeScript. For how frameworks use decorators in real projects, see model objects with TypeScript classes.
Rune AI
Key Insights
- Decorators are functions prefixed with @ that modify classes, methods, or fields at design time.
- Since TypeScript 5.0, native decorators work by default. experimentalDecorators is only needed for legacy decorators.
- A class decorator receives the constructor and a context object, and can return a replacement constructor.
- A method decorator receives the original method and a context object. It can return a replacement method.
- Decorator factories are functions that return decorators, useful for passing configuration like @log(prefix).
Frequently Asked Questions
Do I need to enable a special flag to use decorators?
Can a decorator change the type of the class it decorates?
What is the difference between a decorator and a decorator factory?
Conclusion
Decorators give you a way to attach reusable behavior to classes and their members at design time. A class decorator receives the constructor and a context object, and can replace the constructor entirely. A method decorator receives the original method and a context object, and can return a replacement method. You write a decorator once and apply it anywhere with a single @ symbol. Remember that decorators are functions that run when the class is defined, not when instances are created, and that TypeScript does not reflect decorator-added properties in the type system.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.