TypeScript Decorators

Implement standard TC39 decorators and legacy metadata reflection to intercept class methods, properties, accessors, and constructors.

TL;DR

  1. Apply standard TC39 decorators using the @ symbol prefix.
  2. Intercept and wrap method invocations using ClassMethodDecoratorContext callbacks.
  3. Enable experimentalDecorators only when supporting legacy frameworks like NestJS.

Standard Class Decorators

    Class decorator definition

    Receive target constructor and class context.

    function Frozen<T extends new (...args: any[]) => any>(
      target: T,
      ctx: ClassDecoratorContext
    ) {
      Object.freeze(target);
      Object.freeze(target.prototype);
    }
    Applying class decorator

    Decorate class with prefix at-symbol.

    @Frozen
    class ConfigRegistry {
      appName = "AnalyticsEngine";
    }
    Class replacement decorator

    Return custom subclass to extend behaviors.

    function Timestamped<
      T extends new (...args: any[]) => any
    >(
      Base: T,
      ctx: ClassDecoratorContext
    ) {
      return class extends Base {
        createdAt = new Date();
      };
    }

Method Decorators

    Method logger decorator

    Wrap execution with logging hooks.

    function LogMethod(
      original: Function,
      ctx: ClassMethodDecoratorContext
    ) {
      return function (this: any, ...args: any[]) {
        console.log(`Called ${String(ctx.name)}`, args);
        return original.apply(this, args);
      };
    }
    Applying method decorator

    Annotate instance methods cleanly.

    class TaskRunner {
      @LogMethod
      execute(taskId: string): void {
        console.log(`Running task ${taskId}`);
      }
    }
    Performance timer decorator

    Measure execution duration of methods.

    function Benchmark(
      fn: Function,
      ctx: ClassMethodDecoratorContext
    ) {
      return function (this: any, ...args: any[]) {
        const t0 = performance.now();
        const res = fn.apply(this, args);
        console.log(`${performance.now() - t0}ms`);
        return res;
      };
    }

Field and Accessor Decorators

    Field initializer decorator

    Transform initial values assigned to fields.

    function Upper(
      target: undefined,
      ctx: ClassFieldDecoratorContext
    ) {
      return function (initialValue: string) {
        return initialValue.toUpperCase();
      };
    }
    Applying field decorator

    Decorate instance field declarations.

    class User {
      @Upper
      role: string = "admin";
    }
    Auto-accessor decorator

    Intercept getter and setter on accessors.

    function Validated(
      target: ClassAccessorDecoratorTarget<any, number>,
      ctx: ClassAccessorDecoratorContext
    ) {
      return {
        set(this: any, val: number) {
          if (val < 0) throw new RangeError("Negative");
          target.set.call(this, val);
        }
      };
    }

Legacy Experimental Decorators

    Experimental compiler flags

    Enable legacy flags for Angular and NestJS.

    {
      "compilerOptions": {
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
      }
    }
    Legacy method descriptor

    Mutate property descriptor directly.

    // For experimentalDecorators: true
    // function Log(target: any, key: string, desc: any) {
    //   const fn = desc.value;
    // }
    Metadata reflection import

    Import polyfill for reflect metadata.

    // import "reflect-metadata";
    // Reflect.defineMetadata("key", "val", Target);
    Parameter decorator stub

    Mark parameters for dependency injection.

    // function Inject(token: string) {
    //   return (target: any, key: string, i: number) => {
    //   };
    // }

Tips

  1. Use standard TypeScript 5.0 decorators whenever possible, as they require zero tsconfig.json compiler flags and conform directly to ECMAScript specifications.
  2. Place decorators on separate lines directly above targets with @decorator syntax to preserve clean readability across complex class definitions.

Warnings

  1. Legacy decorators using experimentalDecorators and standard TC39 Stage 3 decorators have incompatible signatures; never mix their runtime helper functions.
  2. Mutating class prototypes directly inside method decorators can trigger unexpected side effects across subclasses sharing the same prototype prototype chain.

In Practice

FAQ