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.

8 min read

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:

jsonjson
{
  "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:

typescripttypescript
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:

typescripttypescript
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:

typescripttypescript
@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:

typescripttypescript
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:

typescripttypescript
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.

texttext
Entering greet
Exiting greet

To 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:

typescripttypescript
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:

typescripttypescript
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.

texttext
first: factory
second: factory
second: decorator
first: decorator

The 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

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).
RunePowered by Rune AI

Frequently Asked Questions

Do I need to enable a special flag to use decorators?

No, not for new code. Since TypeScript 5.0, decorators follow the native TC39 stage 3 proposal and work by default. The older experimentalDecorators flag is only needed for legacy decorators written before TypeScript 5.0.

Can a decorator change the type of the class it decorates?

Not automatically. Decorators can modify runtime behavior, but TypeScript's type system does not see those changes. If a class decorator adds a new property, TypeScript still does not know about it unless you also declare it on the class.

What is the difference between a decorator and a decorator factory?

A decorator is a function that runs when the class is defined. A decorator factory is a function that returns a decorator. Factories are useful when you need to pass configuration options to the decorator, like @log(true) where log returns the actual decorator function.

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.