TypeScript Decorators: Method, Class and Field Examples

A TypeScript decorator is a function that we put above a class or class member with @name, and it can replace that member with a new version. Learn each decorator kind with runnable TypeScript 7 examples, the order decorators run in, the usual compiler errors, and how standard decorators differ from experimentalDecorators used by Angular, NestJS and TypeORM.

Order in which TypeScript decorators are evaluated and applied on the Order class

A TypeScript decorator is a function that we put above a class or a class member with @name, and it can replace that member with a new version. The decorator function runs once, when the class is defined. The new version it returns, such as a wrapper around a method, runs on every call. Since TypeScript 5.0, decorators follow the standard JavaScript design and need no compiler flag.

We use decorators to add the same behavior to many classes without copying code into each method, for example logging, timing or input checks. Angular, NestJS and TypeORM use decorators too, but in an older style that we compare at the end.

The following example adds a @logCalls decorator to a method. The decorator gets the original method and returns a new function that prints the call before it runs the original.

function logCalls(method: (...args: any[]) => any, context: ClassMethodDecoratorContext) {
  const name = String(context.name);
  return function (this: any, ...args: any[]) {
    console.log("Calling " + name + " with " + JSON.stringify(args));
    const result = method.apply(this, args);
    console.log(name + " returned " + result);
    return result;
  };
}

class Cart {
  @logCalls
  total(price: number, quantity: number): number {
    return price * quantity;
  }
}

const cart = new Cart();
const total = cart.total(5, 3);   // prints "Calling total with [5,3]" and "total returned 15", total = 15

The Cart class itself has no logging code. The @logCalls line swapped total() for the wrapper function when the class was defined.

In the next sections, we write a decorator for each kind of class member and see the order in which they run. At the end, we compare them with the older experimentalDecorators style and fix the usual compiler errors.

1. How a TypeScript Decorator Works

A decorator is a plain function with two parameters. The first one, value, is the thing we decorate, for example the method. The second one, context, holds details about it, such as the member name in context.name and the kind in context.kind (“method”, “field” and so on).

When the decorator returns nothing, the member stays as it is. When it returns a new value of the same shape, for example a new function for a method, that value replaces the original. What value holds and what we may return depend on the kind of member.

Decorated membervalue argumentcontext typeCan return
ClassThe class itselfClassDecoratorContextA new class
MethodThe method functionClassMethodDecoratorContextA new function
Getter / setterThe get or set functionClassGetterDecoratorContext / ClassSetterDecoratorContextA new function
FieldundefinedClassFieldDecoratorContextA function that changes the initial value
accessor fieldAn object with get and setClassAccessorDecoratorContextAn object with new get, set and init

Standard decorators cannot decorate parameters, so @Inject() on a constructor parameter works only with the older experimentalDecorators style. We compare both styles in section 8.

2. Project Setup for Decorators in TypeScript 7

Standard decorators need no switch in tsconfig.json. We only leave the experimentalDecorators option off, which is the default, because that option turns on the older style.

The examples use TypeScript 7.0.2 and Node.js 22. TypeScript 7 supports both decorator styles, so decorators written for TypeScript 6 keep working, and TypeScript 7 vs 6 lists the options that did change.

JavaScript engines don’t run decorators yet, so tsc turns each decorator into plain function calls in the output file. For the same reason, node file.ts can’t run a decorated file. That command uses Node.js type stripping, which removes the types but leaves the @ lines, so Node.js stops with a SyntaxError.

3. TypeScript Method Decorator for Timing

A method decorator is the most common kind. It returns a wrapper function, so it can run code before and after every call of the method.

The following example is a @timed decorator that prints how long a method takes. The Playlist class has a find() method that searches a list of song titles. This time we use generics instead of any, so the wrapper keeps the parameter and return types of the method it wraps.

function timed<This, Args extends unknown[], Return>(
  method: (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 {
    const start = performance.now();
    try {
      return method.call(this, ...args);
    } finally {
      const ms = performance.now() - start;
      console.log(name + " took " + ms.toFixed(3) + " ms");
    }
  };
}

class Playlist {
  private songs: string[] = ["Intro", "Hello", "Outro"];

  @timed
  find(title: string): number {
    return this.songs.indexOf(title);
  }
}

const index = new Playlist().find("Hello");   // prints "find took 0.023 ms", index = 1

The wrapper is a normal function that calls method.call(this, …args), so the original method still sees the right this. An arrow function as the wrapper would lose this, a common bug in method decorators. The finally block prints the time even when the method throws.

4. Class Decorator Example

A class decorator gets the class itself. It can store the class somewhere, or it can return a new class that extends it.

Say a notification service picks a handler class by a name from a config file. Instead of a hand-written list of handlers, each handler class registers itself in a Map with a @handler decorator.

const handlers = new Map<string, new () => object>();

function handler<T extends new () => object>(value: T, context: ClassDecoratorContext<T>) {
  handlers.set(String(context.name), value);
}

@handler
class EmailHandler {}

@handler
class SmsHandler {}

const names = [...handlers.keys()];                        // ["EmailHandler", "SmsHandler"]
const created = new (handlers.get("SmsHandler")!)();       // created.constructor.name = "SmsHandler"

The @handler decorator returns nothing, so both classes stay unchanged. The map is filled when the module loads, before any code creates an object.

5. Field Decorator With an Initializer

A field decorator runs before the field has a value, so its value argument is always undefined. To change the field, the decorator returns a function that gets the starting value and returns the value to use instead.

The following example trims spaces from the starting value of a title field.

function trimSpaces<This>(_value: undefined, context: ClassFieldDecoratorContext<This, string>) {
  return function (initialValue: string): string {
    return initialValue.trim();
  };
}

class Song {
  @trimSpaces
  title = "  Hello  ";
}

const song = new Song();
const title = song.title;      // "Hello"
song.title = "  Bye  ";
const after = song.title;      // "  Bye  " (later assignments are not trimmed)

The second result shows the limit of a field decorator. A plain field has no setter to wrap, so the decorator sees only the starting value.

6. Accessor Decorator Factory With Arguments

An accessor field looks like a normal field from outside, but TypeScript creates a hidden getter and setter for it. An accessor decorator can replace that setter, so it sees every write, which a field decorator can’t.

When a decorator takes arguments, such as @clamp(0, 100), the function clamp is a decorator factory. It takes our arguments and returns the decorator, which is a function with the usual (target, context) parameters.

The following example keeps the volume of a speaker between a minimum and a maximum. The target argument holds the original get and set functions. The decorator returns init to fix the starting value and a new set to fix every later value.

function clamp(min: number, max: number) {
  return function <This>(
    target: ClassAccessorDecoratorTarget<This, number>,
    context: ClassAccessorDecoratorContext<This, number>
  ): ClassAccessorDecoratorResult<This, number> {
    return {
      set(value: number) {
        target.set.call(this, Math.min(max, Math.max(min, value)));
      },
      init(value: number) {
        return Math.min(max, Math.max(min, value));
      },
    };
  };
}

class Speaker {
  @clamp(0, 100)
  accessor volume = 150;
}

const speaker = new Speaker();
const start = speaker.volume;   // 100
speaker.volume = -20;
const low = speaker.volume;     // 0

The decorator returns no get, so reads still use the original getter. With a factory, one decorator serves different settings, for example @clamp(0, 10) for a rating and @clamp(0, 100) for a volume.

7. The Order Decorators Run In

Decorators run in two steps, and both steps happen once, when the class is defined. First, TypeScript evaluates every @… expression from top to bottom, so factories such as trace(“class”) are called. After that, it applies the returned decorators to the members and to the class.

The following example uses a trace() factory that prints a line in each step. We put it on the Order class, on a field, twice on a method and on a static method.

function trace(label: string) {
  console.log("evaluate " + label);
  return function (_value: unknown, context: DecoratorContext) {
    console.log("apply    " + label + " on " + context.kind + " " + String(context.name));
  };
}

@trace("class")
class Order {
  @trace("field") id = 1;

  @trace("first")
  @trace("second")
  save() {}

  @trace("static") static create() {}
}
evaluate class
evaluate field
evaluate first
evaluate second
evaluate static
apply    static on method create
apply    second on method save
apply    first on method save
apply    field on field id
apply    class on class Order

Static members are decorated before instance members, and the class decorator runs last. On save(), the decorator closest to the method (second) is applied first, so first wraps its result. For example, with @logCalls above @timed, the logging wrapper is the outer one.

Order in which TypeScript decorators are evaluated and applied on the Order class
TypeScript evaluates decorator expressions from top to bottom, then applies them to members first and to the class last.

8. Standard Decorators vs experimentalDecorators

Before version 5.0, TypeScript had its own decorator design, turned on with the experimentalDecorators option. The two designs have different function signatures, and each style rejects decorators written for the other.

PointStandard decoratorsexperimentalDecorators (legacy)
Compiler optionNone, on by default since TypeScript 5.0“experimentalDecorators”: true
Method decorator signature(value, context)(target, propertyKey, descriptor), where the descriptor holds the method in descriptor.value
How it changes a methodReturns a new functionChanges the PropertyDescriptor
Parameter decoratorsNot supportedSupported, e.g. @Inject()
emitDecoratorMetadata (type info for frameworks)Not supportedSupported
accessor fieldsSupportedNot supported
Used byNew code and newer librariesAngular, NestJS, TypeORM

The framework decides which style a project uses.

  • In an Angular, NestJS or TypeORM project, keep experimentalDecorators on. The Angular CLI and the NestJS starter turn it on, and TypeORM needs it with emitDecoratorMetadata.
  • In our own code, use standard decorators. They follow the JavaScript proposal, so they keep working when engines run decorators natively.

9. Decorator Errors and How to Fix Them

Most decorator errors come from mixing the two styles. A legacy decorator with three parameters fails with standard decorators, which call it with two arguments.

src/cart.ts(5,3): error TS1241: Unable to resolve signature of method decorator when called as an expression.
  The runtime will invoke the decorator with 2 arguments, but the decorator expects 3.

We either rewrite the decorator with the (value, context) signature, or turn on experimentalDecorators when a framework needs it.

A decorator on a constructor parameter, or on a function outside a class, gives TS1206, because standard decorators work only on classes and class members.

src/mailer.ts(5,15): error TS1206: Decorators are not valid here.

Running a decorated file with node src/01-first-decorator.ts fails at runtime, as we saw in section 2. We build with tsc first and run the .js output.

file:///.../decorators/src/01-first-decorator.ts:13
  @logCalls
  ^

SyntaxError: Invalid or unexpected token

10. Conclusion

A TypeScript decorator gets a class member and a context object, and it can return a replacement for that member. Method decorators wrap calls, and accessor decorators see every write. All decorators run once, when the class is defined.

Standard decorators need no compiler option since TypeScript 5.0. Angular, NestJS and TypeORM still need experimentalDecorators and the legacy (target, propertyKey, descriptor) style. The TypeScript interface and method override posts cover the class basics that decorators build on.

11. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.