Technology · TypeScript

TypeScript Decorators

Understand decorators, metadata, class transformations, and real-world patterns.

TL;DR
  1. 01Use decorators to annotate and modify classes and methods.
  2. 02Enable experimentalDecorators in tsconfig.json to use them.
  3. 03Apply metadata to decorate classes for frameworks and libraries.

Class Decorators

  • Enable decorators in tsconfig.json first.
    {
      "compilerOptions": {
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
      }
    }
  • Create a class decorator that receives the class constructor.
    function Sealed(constructor: Function) {
      Object.seal(constructor);
      Object.seal(constructor.prototype);
    }
    
    @Sealed
    class User {
      name = "Alice";
    }
  • Modify or wrap classes to add functionality.
    function Timestamped<T extends { new(...args: any[]): {} }>(
      constructor: T
    ) {
      return class extends constructor {
        createdAt = new Date();
      };
    }
  • Factory decorators can return new classes with enhanced features.

Method and Property Decorators

  • Create method decorators to intercept function calls.
    function LogCalls(
      target: any,
      propertyKey: string,
      descriptor: PropertyDescriptor
    ) {
      const original = descriptor.value;
      
      descriptor.value = function(...args: any[]) {
        console.log(`Calling ${propertyKey}`, args);
        return original.apply(this, args);
      };
    }
    
    class User {
      @LogCalls
      setName(name: string) {
        this.name = name;
      }
    }
  • Property decorators receive the target and property name.
    function Validate(target: any, propertyKey: string) {
      let value: any;
      
      Object.defineProperty(target, propertyKey, {
        get() { return value; },
        set(newVal: any) {
          if (newVal.length < 3) throw new Error("Too short");
          value = newVal;
        }
      });
    }

Metadata and Reflection

  • Store metadata on classes and methods using the reflect-metadata library.
    import "reflect-metadata";
    
    function Route(path: string) {
      return function(target: any, propertyKey: string) {
        Reflect.defineMetadata("route:path", path, target, propertyKey);
      };
    }
    
    class Controller {
      @Route("/users")
      getUsers() { }
    }
  • Retrieve metadata to build frameworks and libraries.
    const path = Reflect.getMetadata("route:path", Controller.prototype, "getUsers");
    console.log(path); // "/users"
  • Use emitDecoratorMetadata to capture parameter and return types.
    function ValidateTypes(target: any, propertyKey: string) {
      const types = Reflect.getMetadata("design:paramtypes", target, propertyKey);
      // Types are [String, Number, etc.]
    }

Decorator Composition

  • Stack multiple decorators on the same class or method.
    @Sealed
    @Timestamped
    class User {
      @LogCalls
      @Validate
      setName(name: string) {
        this.name = name;
      }
    }
  • Decorators execute from bottom to top on the target.
  • Compose decorators to build complex functionality.
    function Chain(...decorators: any[]) {
      return (target: any, propertyKey: string) => {
        decorators.forEach(d => d(target, propertyKey));
      };
    }
  • Use composition to keep decorators focused on single concerns.

Real-World Patterns

  • Build validation decorators for class properties.
    function MinLength(min: number) {
      return function(target: any, propertyKey: string) {
        Reflect.defineMetadata(`validate:${propertyKey}`, { min }, target);
      };
    }
    
    class User {
      @MinLength(3)
      name: string;
    }
  • Create caching decorators for expensive methods.
    function Memoize(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
      const original = descriptor.value;
      const cache = new Map();
      
      descriptor.value = function(...args: any[]) {
        const key = JSON.stringify(args);
        if (cache.has(key)) return cache.get(key);
        
        const result = original.apply(this, args);
        cache.set(key, result);
        return result;
      };
    }
  • Use decorators in frameworks like NestJS for dependency injection.

Tip: Use decorators sparingly and for cross-cutting concerns like logging, validation, or caching — avoid using them for simple business logic.

Warning: Decorators are experimental and may change in future TypeScript versions, so use with caution in production code.

TypeScript Declaration FilesTypeScript Enums Best Practices