2 min read

TypeScript Decorators Metadata

TypeScript has an experimental feature called Decorator Metadata. When enabled, the compiler emits design-time type information for decorated declarations. This information can be accessed at runtime using the Reflect API.

This feature is the backbone of many Dependency Injection frameworks (like Angular and NestJS) and ORMs (like TypeORM).

1. Setup

To use metadata, you need two things:

  1. Polyfill: The Reflect API for metadata is not yet standard. You must install reflect-metadata.

    npm install reflect-metadata
  2. Configuration: Enable specific flags in tsconfig.json.

    {
      "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
      }
    }

Important: You must import reflect-metadata at the top of your entry file (e.g., import "reflect-metadata";).

2. Built-in Metadata Keys

When emitDecoratorMetadata is on, TypeScript automatically adds three metadata keys to decorated members:

  • "design:type": The type of the property.
  • "design:paramtypes": An array of types for constructor or method parameters.
  • "design:returntype": The return type of a method.

3. Example: Inspecting Types

Let's create a simple decorator that logs the types TypeScript inferred.

import "reflect-metadata";

function LogType(target: any, key: string) {
  const type = Reflect.getMetadata("design:type", target, key);
  console.log(`${key} type: ${type.name}`);
}

function LogParameters(target: any, key: string, descriptor: PropertyDescriptor) {
  const types = Reflect.getMetadata("design:paramtypes", target, key);
  const typeNames = types.map((t: any) => t.name).join(", ");
  console.log(`${key} parameters: [${typeNames}]`);
}

class Demo {
  @LogType
  public name: string = "Alice";

  @LogParameters
  add(a: number, b: number): number {
    return a + b;
  }
}

// Output:
// name type: String
// add parameters: [Number, Number]

4. Practical Use Case: Dependency Injection

This is a simplified example of how frameworks like Angular use metadata to inject dependencies automatically based on the constructor parameter types.

import "reflect-metadata";

// 1. The Service
class Logger {
  log(msg: string) { console.log("LOG:", msg); }
}

// 2. The Decorator (marks the class as injectable)
function Injectable(constructor: Function) {
  // In a real framework, this would register the class in a container
}

// 3. The Consumer
@Injectable
class UserService {
  // TypeScript emits metadata saying the first param is of type 'Logger'
  constructor(private logger: Logger) {}

  saveUser() {
    this.logger.log("User saved");
  }
}

// 4. The "Framework" (Container)
function resolve<T>(target: any): T {
  // Get the types of the constructor parameters
  const tokens = Reflect.getMetadata("design:paramtypes", target) || [];

  // Instantiate dependencies (simplified: assumes no dependencies for dependencies)
  const injections = tokens.map((token: any) => new token());

  // Instantiate the target class with dependencies
  return new target(...injections);
}

// Usage
const userService = resolve<UserService>(UserService);
userService.saveUser(); // Output: LOG: User saved

5. Limitations

  • Interfaces: Interfaces disappear at runtime, so design:type will often return Object if the type is an interface.
  • Circular Dependencies: If two classes depend on each other, the metadata might be undefined at runtime due to module loading order.

programming/javascript/typescript/typescript programming/javascript/typescript/decorators