← Back to Course

✨ Decorators & Metadata

Decorators are an experimental TypeScript feature that allows you to add metadata and modify classes, methods, properties, and parameters. They're heavily used in frameworks like Angular, NestJS, and TypeORM.

Note: Decorators are experimental. You must enable them in tsconfig.json with "experimentalDecorators": true

Enabling Decorators

tsconfig.json Configuration

{
  "compilerOptions": {
    "target": "ES2020",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true  // For metadata reflection
  }
}

Class Decorators

Class decorators are applied to the class constructor:

Example: Class Decorator

// Decorator factory function
function Component(config: { selector: string; template: string }) {
  return function (constructor: Function) {
    console.log(`Registering component: ${config.selector}`);
    // Add metadata or modify the class
    (constructor as any).metadata = config;
  };
}

// Using the decorator
@Component({
  selector: 'app-hero',
  template: '<div>Hero Component</div>'
})
class HeroComponent {
  name: string = "Hero";
}

// Decorator to make class immutable
function Frozen(constructor: Function) {
  Object.freeze(constructor);
  Object.freeze(constructor.prototype);
}

@Frozen
class ImmutableClass {
  value: number = 42;
}

Method Decorators

Method decorators can modify or observe method behavior:

Example: Method Decorator

// Log method calls
function Log(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;
  
  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey} with args:`, args);
    const result = originalMethod.apply(this, args);
    console.log(`${propertyKey} returned:`, result);
    return result;
  };
  
  return descriptor;
}

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

const calc = new Calculator();
calc.add(5, 3);
// Logs: "Calling add with args: [5, 3]"
// Logs: "add returned: 8"

Property Decorators

Property decorators add metadata to class properties:

Example: Property Decorator

// Mark property as required
function Required(target: any, propertyKey: string) {
  let value: any;
  
  const getter = () => {
    return value;
  };
  
  const setter = (newValue: any) => {
    if (newValue === null || newValue === undefined) {
      throw new Error(`${propertyKey} is required!`);
    }
    value = newValue;
  };
  
  Object.defineProperty(target, propertyKey, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true
  });
}

class User {
  @Required
  username: string;
  
  constructor(username: string) {
    this.username = username;
  }
}

// const user = new User(null);  // Error: username is required!

Parameter Decorators

Parameter decorators mark function parameters:

Example: Parameter Decorator

// Validate parameter
function Validate(
  target: any,
  propertyKey: string,
  parameterIndex: number
) {
  const existingParams = Reflect.getMetadata("validate", target, propertyKey) || [];
  existingParams.push(parameterIndex);
  Reflect.defineMetadata("validate", existingParams, target, propertyKey);
}

class GameService {
  createGame(
    @Validate name: string,
    @Validate maxPlayers: number
  ) {
    // Validation happens before method executes
    return { name, maxPlayers };
  }
}

Decorator Factories

Decorator factories allow customization:

Example: Decorator Factory

// Decorator factory with options
function Throttle(milliseconds: number) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const original Method = descriptor.value;
    let timeout: NodeJS.Timeout | null = null;
    
    descriptor.value = function (...args: any[]) {
      if (timeout) {
        clearTimeout(timeout);
      }
      
      timeout = setTimeout(() => {
        originalMethod.apply(this, args);
      }, milliseconds);
    };
    
    return descriptor;
  };
}

class SearchBox {
  @Throttle(300)  // Wait 300ms before executing
  onSearch(query: string): void {
    console.log(`Searching for: ${query}`);
  }
}

// Advanced decorator factory
function Cache(duration: number = 60000) {
  const cache = new Map<string, { value: any; expires: number }>();
  
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;
    
    descriptor.value = function (...args: any[]) {
      const key = JSON.stringify(args);
      const cached = cache.get(key);
      
      if (cached && cached.expires > Date.now()) {
        console.log(`Cache hit for ${propertyKey}`);
        return cached.value;
      }
      
      const result = originalMethod.apply(this, args);
      cache.set(key, {
        value: result,
        expires: Date.now() + duration
      });
      
      return result;
    };
    
    return descriptor;
  };
}

class DataService {
  @Cache(5000)  // Cache for 5 seconds
  fetchData(id: number): any {
    console.log(`Fetching data for ID: ${id}`);
    return { id, data: "Some data..." };
  }
}

Real-World Use Cases

Example: Practical Decorators

// Dependency Injection (like Angular)
function Injectable() {
  return function (constructor: Function) {
    // Register service in DI container
    console.log(`Registering ${constructor.name} as injectable`);
  };
}

@Injectable()
class GameService {
  getPlayers() {
    return ["Player1", "Player2"];
  }
}

// Route decorators (like NestJS)
function Controller(route: string) {
  return function (constructor: Function) {
    (constructor as any).route = route;
  };
}

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

@Controller('/api/players')
class PlayerController {
  @Get('/')
  getAll() {
    return { players: [] };
  }
  
  @Get('/:id')
  getOne(id: string) {
    return { id, name: "Player" };
  }
}

// Entity decorators (like TypeORM)
function Entity(tableName: string) {
  return function (constructor: Function) {
    (constructor as any).tableName = tableName;
  };
}

function Column(options: { type: string; nullable?: boolean }) {
  return function (target: any, propertyKey: string) {
    // Store column metadata
  };
}

@Entity('users')
class UserEntity {
  @Column({ type: 'int' })
  id: number;
  
  @Column({ type: 'varchar', nullable: false })
  username: string;
  
  @Column({ type: 'varchar', nullable: false })
  email: string;
}

Decorator Composition

Example: Multiple Decorators

// Multiple decorators on same target
function First() {
  console.log("First(): factory");
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("First(): called");
  };
}

function Second() {
  console.log("Second(): factory");
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("Second(): called");
  };
}

class Example {
  @First()
  @Second()
  method() {}
}

// Output:
// First(): factory
// Second(): factory
// Second(): called  (executed bottom-to-top)
// First(): called

🎯 Key Concepts

  • Decorators are experimental - enable in tsconfig.json
  • Use @DecoratorName syntax before declarations
  • Class decorators modify or observe class constructors
  • Method decorators wrap or modify method behavior
  • Property decorators add metadata to properties
  • Decorator factories return decorator functions
  • Multiple decorators execute bottom-to-top
  • Heavily used in Angular, NestJS, TypeORM
← Back to Intermediate Course