TypeScript Decorators — Complete Guide

Sanjeev SharmaSanjeev Sharma
6 min read

Advertisement

Decorators bring metaprogramming to TypeScript, enabling you to modify classes, methods, and properties at design time. They're powerful for frameworks like NestJS, TypeORM, and class-validator, and understanding them unlocks advanced patterns.

What Are Decorators?

Decorators are functions that augment classes, methods, properties, or parameters. They're prefixed with an @ symbol and execute when the decorated item is defined, not when it's used.

// Enable decorators in tsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

// Simple decorator
function logged(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args: any[]) {
    console.log(`Calling ${propertyKey} with`, args);
    return original.apply(this, args);
  };
  return descriptor;
}

class Calculator {
  @logged
  add(a: number, b: number): number {
    return a + b;
  }
}

const calc = new Calculator();
calc.add(2, 3); // Logs: Calling add with [2, 3]

Class Decorators

// Basic class decorator
function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

@sealed
class User {
  name: string = "";
}

// Decorator with arguments
function entity(tableName: string) {
  return function<T extends { new(...args: any[]): {} }>(constructor: T) {
    return class extends constructor {
      static tableName = tableName;
    };
  };
}

@entity("users")
class UserEntity {
  id: number = 0;
  name: string = "";
}

console.log((UserEntity as any).tableName); // "users"

Property Decorators

// Property decorator
function Range(min: number, max: number) {
  return function(target: Object, propertyKey: string | symbol) {
    let value: any;

    const getter = function() {
      return value;
    };

    const setter = function(newVal: any) {
      if (newVal < min || newVal > max) {
        throw new Error(
          `${String(propertyKey)} must be between ${min} and ${max}`
        );
      }
      value = newVal;
    };

    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true,
    });
  };
}

class Product {
  @Range(0, 100)
  discount: number = 0;
}

const product = new Product();
product.discount = 50; // OK
product.discount = 150; // Throws error

Method Decorators

// Timing decorator
function timed(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;

  descriptor.value = async function(...args: any[]) {
    const start = performance.now();
    const result = await original.apply(this, args);
    const end = performance.now();
    console.log(`${propertyKey} took ${end - start}ms`);
    return result;
  };

  return descriptor;
}

class DataService {
  @timed
  async fetchUsers(): Promise<any[]> {
    return new Promise((resolve) => {
      setTimeout(() => resolve([]), 1000);
    });
  }
}

// Authorization decorator
function authorize(roles: string[]) {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const original = descriptor.value;

    descriptor.value = function(...args: any[]) {
      const user = (this as any).currentUser;
      if (!user || !roles.includes(user.role)) {
        throw new Error("Unauthorized");
      }
      return original.apply(this, args);
    };

    return descriptor;
  };
}

class AdminService {
  currentUser = { role: "admin" };

  @authorize(["admin"])
  deleteUser(userId: number): void {
    console.log(`Deleting user ${userId}`);
  }
}

Parameter Decorators

// Parameter decorator for validation
function Validate(target: any, propertyKey: string | symbol, parameterIndex: number) {
  const existingMetadata = Reflect.getOwnMetadata("validate", target, propertyKey) || [];
  existingMetadata.push(parameterIndex);
  Reflect.defineMetadata("validate", existingMetadata, target, propertyKey);
}

function ValidateEmail(target: any, propertyKey: string | symbol, parameterIndex: number) {
  const existingMetadata = Reflect.getOwnMetadata("validateEmail", target, propertyKey) || [];
  existingMetadata.push(parameterIndex);
  Reflect.defineMetadata("validateEmail", existingMetadata, target, propertyKey);
}

class UserController {
  createUser(@ValidateEmail email: string): void {
    console.log(`Creating user with email: ${email}`);
  }
}

Advanced Decorator Patterns

Dependency Injection

// Simplified DI decorator
class Container {
  private services = new Map<string, any>();

  register<T>(key: string, factory: () => T): void {
    this.services.set(key, factory());
  }

  get<T>(key: string): T {
    return this.services.get(key);
  }
}

const container = new Container();

function Injectable(target: Function) {
  const instance = new (target as any)();
  container.register(target.name, () => instance);
}

interface Logger {
  log(message: string): void;
}

@Injectable
class ConsoleLogger implements Logger {
  log(message: string): void {
    console.log(message);
  }
}

function Inject(serviceKey: string) {
  return function(target: Object, propertyKey: string | symbol) {
    Object.defineProperty(target, propertyKey, {
      get: () => container.get(serviceKey),
    });
  };
}

class UserService {
  @Inject("ConsoleLogger")
  logger!: Logger;

  getUser(): void {
    this.logger.log("Getting user");
  }
}

Type-Safe Decorators with Reflect

import "reflect-metadata";

// Reflect metadata for type info
function Serializable(target: Function) {
  const types = Reflect.getOwnMetadata("design:paramtypes", target);
  console.log("Constructor parameters:", types);
}

// Property type metadata
function Column(target: Object, propertyKey: string) {
  const type = Reflect.getMetadata("design:type", target, propertyKey);
  console.log(`${String(propertyKey)} is type ${type.name}`);
}

@Serializable
class User {
  @Column
  id: number = 0;

  @Column
  name: string = "";
}

Real-World Examples

TypeORM-Style Decorators

function Entity(tableName: string) {
  return function<T extends { new(...args: any[]): {} }>(constructor: T) {
    (constructor as any).tableName = tableName;
    return constructor;
  };
}

function Column(type?: "string" | "number" | "date") {
  return function(target: Object, propertyKey: string | symbol) {
    if (!Reflect.hasMetadata("columns", target.constructor)) {
      Reflect.defineMetadata("columns", [], target.constructor);
    }
    const columns = Reflect.getMetadata("columns", target.constructor);
    columns.push({ name: propertyKey, type });
  };
}

@Entity("users")
class User {
  @Column("number")
  id: number = 0;

  @Column("string")
  name: string = "";

  @Column("date")
  createdAt: Date = new Date();
}

Route Decorators

function Get(path: string) {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.value.method = "GET";
    descriptor.value.path = path;
    return descriptor;
  };
}

function Post(path: string) {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.value.method = "POST";
    descriptor.value.path = path;
    return descriptor;
  };
}

class UserController {
  @Get("/users")
  listUsers(): void {}

  @Post("/users")
  createUser(): void {}
}

Best Practices

  1. Use decorators sparingly - they can make code harder to understand
  2. Document decorator behavior - side effects aren't obvious
  3. Combine with types - use generics for type-safe decorators
  4. Test decorated classes thoroughly - decorators modify behavior

Limitations and Alternatives

  • Decorators are experimental - API may change
  • Performance overhead - decorators add runtime cost
  • Complexity - can make debugging harder
  • Consider alternatives - composition or middleware might be clearer

FAQ

Q: Are decorators production-ready? A: Yes, with experimentalDecorators enabled. Major frameworks use them. Stage 3 proposal means standardization is in progress.

Q: Can I stack multiple decorators? A: Yes, they execute bottom-to-top. @decorator1 @decorator2 calls decorator2 first, then decorator1.

Q: What's the performance impact? A: Minimal for most use cases. Decorators execute once at class definition, not per method call. Profile your specific use case if performance is critical.


Decorators transform TypeScript from a language with types into a metaprogramming powerhouse. Whether building frameworks or enterprise applications, mastering decorators opens advanced architectural patterns. Use them judiciously for maximum impact.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro