Decorators — TypeScript · Foundations

@Controller('cats') and @Get(':id') are ordinary functions that run when a class is defined. Write your own: a class decorator and method decorators that record routes, and one that wraps a method to log its calls.

What you will learn

Read the theory for Decorators

All TypeScript lessons

All Foundations courses

loading types…

What you'll learn

  • Write class and method decorators, and decorator factories that take arguments
  • Explain when decorators run and in what order, and what each kind receives
  • Wrap a method through its property descriptor without losing this or its return value

Decorators

A NestJS controller is mostly decorators: @Controller('cats') on the class, @Get(':id') on a method, @Param('id') on a parameter. They look like configuration, and it is tempting to treat them as magic. They are not: a decorator is an ordinary function that TypeScript calls when the class is defined, handing it the class, the method or the parameter it sits on. What Nest's decorators do with that is simple: they write down facts ("this class answers under /cats", "this method answers GET :id") for the framework to read later. This lesson writes the same kind of decorators, which is the surest way to stop finding them mysterious.

Two flavours, one of them Nest's

TypeScript has two decorator systems. The one NestJS is built on is enabled by the experimentalDecorators compiler option (with emitDecoratorMetadata, lesson 8); both are on in this editor, as in every Nest project. TypeScript 5 also supports the newer standard decorators, with different signatures, used when that option is off. Everything here is the experimental kind.

What each kind receives

KindCalled withReturn
classthe class (its constructor function)nothing, or a replacement class
methodthe prototype, the method's name, its property descriptornothing, or a replacement descriptor
propertythe prototype, the property's namenothing
parameterthe prototype, the method's name, the parameter's indexnothing

For a method, target is the class's prototype, the object its instances share, so the class itself is target.constructor. The descriptor is the object that holds the method: descriptor.value is the function.

Factories

@Get(':id') has an argument, so Get is not itself the decorator; it is a factory that returns one:

function Tag(label: string): ClassDecorator {
  return (target) => {
    tags.set(target, label);
  };
}

@Tag('internal')
class AuditService {}

Tag('internal') runs first and returns the decorator, which TypeScript then applies to the class.

When they run, and in what order

Decorators run once, when the class definition is evaluated, which is when its module is imported: before any instance exists and before any method is called. Within a class, the member decorators run first, in the order the members are declared, and the class decorator last. On one member with several decorators, the factories are evaluated top to bottom and the decorators applied bottom to top, so the one nearest the method wraps it first.

That timing is why a decorator can only record things about a method, or replace it; anything that should happen per call has to be put inside a wrapper.

Keeping facts per class

A decorator needs somewhere to write. A WeakMap keyed by the class works well: each class gets its own entry, and a class that is no longer used can be garbage-collected with its entry. (Nest writes into the class's metadata instead, which lesson 8 is about.)

Wrapping a method

A method decorator can replace descriptor.value with a new function that does something around the original:

function Timed(times: number[]) {
  return (target: object, key: string | symbol, descriptor: PropertyDescriptor): void => {
    const original = descriptor.value;
    descriptor.value = function (this: unknown, ...args: unknown[]) {
      const started = performance.now();
      try {
        return original.apply(this, args);
      } finally {
        times.push(performance.now() - started);
      }
    };
  };
}

Two details carry the weight. The wrapper is a function, not an arrow, so it receives the this of the call, the instance; and it calls the original with original.apply(this, args), passing that this on and returning the result. An arrow function, or a plain original(...args), loses the instance, and the method's first this.something fails.

Your task

The work is in routing.ts; the spec decorates four controllers with your decorators.

  1. @Controller(prefix) records the prefix for the class.
  2. @Get(path) and @Post(path) record a route for the method's class: the HTTP method, the path ('' when none is given) and the method's name as handler.
  3. routesOf(controller) answers every route of a controller in the order its methods were declared, the path being the prefix and the route's path joined by single slashes and starting with one (cats + :id is /cats/:id, '' + health is /health, /owners/ + /:id/cats is /owners/:id/cats). For a class without @Controller it throws Error("<ClassName> is not a controller").
  4. @Log(entries) wraps the method: every call adds "<method>(<arguments as JSON, joined by ', '>)" to entries, then runs the original with the same this and arguments and returns its result.

When it fails

  • "the tests could not run: not written yet": the decorators run as soon as the spec defines its classes, so a decorator that throws stops everything before a single test.
  • A route appears under the wrong controller, or under none: it was recorded against target, the prototype, while routesOf looks up the class. Use target.constructor.
  • "keeps this" fails with Cannot read properties of undefined: the wrapper is an arrow function, or calls the original without apply(this, …).
  • /cats/ or //health: empty segments were joined. Trim the slashes and drop the empty parts.

Remember

  • A decorator is a function run once, when the class is defined, with what it decorates.
  • A factory takes arguments and returns the decorator.
  • A method decorator gets the prototype; the class is target.constructor.
  • Wrap with a function and original.apply(this, args), or this is lost.
Stuck? Show a hint

A decorator factory is a function that returns the decorator: Controller(prefix) returns (target) => { … }. A method decorator receives the class's prototype, the method's name and its descriptor; target.constructor is the class. WeakMap<Function, …> keyed by that class keeps the prefix and a list of routes. To wrap, keep descriptor.value, then replace it with a function(this: unknown, ...args) that records the call and returns original.apply(this, args).