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:
-
Polyfill: The
ReflectAPI for metadata is not yet standard. You must installreflect-metadata.npm install reflect-metadata -
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:typewill often returnObjectif the type is an interface. - Circular Dependencies: If two classes depend on each other, the metadata might be
undefinedat runtime due to module loading order.
programming/javascript/typescript/typescript programming/javascript/typescript/decorators