Decorators: A TypeScript feature (borrowed from JavaScript) that allows you to attach metadata or modify the behavior of classes, methods, accessors, properties, or parameters at design time.
@ (e.g., @MyDecorator).experimentalDecorators compiler option in tsconfig.json.Add the following to tsconfig.json:
{ "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true }
} Note:emitDecoratorMetadata is needed for metadata-related decorators (e.g., with reflect-metadata).
Class Decorators: Applied to a class constructor, used to modify or annotate the entire class.
(constructor: Function) => void | Function.Method Decorators: Applied to methods, used to modify behavior or add metadata.
(target: any, propertyKey: string, descriptor: PropertyDescriptor) => void | PropertyDescriptor.Accessor Decorators: Applied to getters/setters, used to control property access.
Property Decorators: Applied to class properties, used to annotate or validate.
(target: any, propertyKey: string) => void.Parameter Decorators: Applied to method parameters, used for metadata or validation.
(target: any, propertyKey: string, parameterIndex: number) => void.
// Class, Method, Accessor, Property, and Parameter Decorators
// Class Decorator
function LogClass(constructor: Function) { console.log(`Class created: ${constructor.name}`);
} // Method Decorator
function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; descriptor.value = function (...args: any[]) { console.log(`Calling ${propertyKey} with args: ${args}`); return originalMethod.apply(this, args); };
} // Accessor Decorator
function ReadOnly(target: any, propertyKey: string, descriptor: PropertyDescriptor) { descriptor.set = undefined; // Prevent setting
} // Property Decorator
function Required(target: any, propertyKey: string) { // Store metadata (simplified example) console.log(`Property ${propertyKey} is required`);
} // Parameter Decorator
function LogParam(target: any, propertyKey: string, parameterIndex: number) { console.log(`Parameter ${parameterIndex} in ${propertyKey} is being decorated`);
} @LogClass
class User { @Required name: string; private _age: number; constructor(name: string, age: number) { this.name = name; this._age = age; } @ReadOnly get age(): number { return this._age; } @LogMethod greet(@LogParam message: string): string { return `Hello, ${this.name}! ${message}`; }
} // Testing decorators
const user = new User("kristal", 30);
console.log(user.greet("Welcome!"));
console.log(user.age); // Getter works
// user.age = 40; // Error: setter is undefined Output:
Class created: User
Property name is required
Parameter 0 in greet is being decorated
Calling greet with args: Welcome!
Hello, kristal! Welcome!
30 Metadata: Decorators can attach metadata to classes, methods, or properties using the reflect-metadata library, enabling runtime introspection.
Dependency Injection (DI): Decorators annotate classes or properties to manage dependencies, often with an inversion-of-control (IoC) container.
Library:reflect-metadata is commonly used for metadata storage/retrieval.
Setup: Install reflect-metadata:
npm install reflect-metadata
// Metadata and Dependency Injection with Decorators
import "reflect-metadata"; // Simple DI container
const container = new Map(); function Injectable(key: string) { return function (constructor: Function) { container.set(key, new constructor()); };
} function Inject(key: string) { return function (target: any, propertyKey: string) { Reflect.defineMetadata("inject:key", key, target, propertyKey); };
} function LogMetadata(target: any, propertyKey: string, parameterIndex: number) { Reflect.defineMetadata("param:log", true, target, propertyKey);
} // Service
@Injectable("logger")
class LoggerService { log(message: string): void { console.log(`Log: ${message}`); }
} // Consumer class
class App { @Inject("logger") private logger: LoggerService; constructor() { // Resolve dependency const key = Reflect.getMetadata("inject:key", this, "logger"); this.logger = container.get(key); } @LogMetadata run(@LogMetadata message: string): void { const hasLog = Reflect.getMetadata("param:log", this, "run"); if (hasLog) { this.logger.log(message); } }
} // Testing
const app = new App();
app.run("Test message"); Output:
Log: Test message Project Structure:
ts-decorators/
├── src/
│ └── main.ts
├── tsconfig.json
└── package.json main.ts:
// Comprehensive Decorator Example
import "reflect-metadata"; // DI Container
const container = new Map(); // Class Decorator
function Injectable(key: string) { return function (constructor: Function) { console.log(`Registering ${constructor.name} with key: ${key}`); container.set(key, new constructor()); };
} // Property Decorator for DI
function Inject(key: string) { return function (target: any, propertyKey: string) { Reflect.defineMetadata("inject:key", key, target, propertyKey); };
} // Method Decorator for Logging
function LogExecution(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; descriptor.value = async function (...args: any[]) { console.log(`Executing ${propertyKey} with args: ${args}`); const result = await originalMethod.apply(this, args); console.log(`Result of ${propertyKey}: ${result}`); return result; };
} // Accessor Decorator
function ValidateAge(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalSet = descriptor.set; descriptor.set = function (value: number) { if (value < 0) throw new Error("Age cannot be negative"); originalSet!.call(this, value); };
} // Parameter Decorator
function RequiredParam(target: any, propertyKey: string, parameterIndex: number) { const existing = Reflect.getMetadata("required:params", target, propertyKey) || []; existing.push(parameterIndex); Reflect.defineMetadata("required:params", existing, target, propertyKey);
} // Service
@Injectable("logger")
class LoggerService { log(message: string): void { console.log(`Log: ${message}`); }
} // Main Class
@Injectable("user")
class User { @Inject("logger") private logger: LoggerService; private _age: number; constructor() { const key = Reflect.getMetadata("inject:key", this, "logger"); this.logger = container.get(key); } @ValidateAge set age(value: number) { this._age = value; } get age(): number { return this._age; } @LogExecution greet(@RequiredParam name: string): string { const requiredParams = Reflect.getMetadata("required:params", this, "greet") || []; if (requiredParams.includes(0) && !name) { throw new Error("Name is required"); } this.logger.log(`Greeting ${name}`); return `Hello, ${name}!`; }
} // Testing
try { const user = new User(); user.age = 30; console.log(user.greet("kristal")); // user.greet(""); // Throws error // user.age = -5; // Throws error
} catch (e) { console.error(`Error: ${e.message}`);
} package.json:
{ "name": "ts-decorators", "version": "1.0.0", "scripts": { "start": "tsc && node dist/main.js", "build": "tsc", "watch": "tsc --watch" }, "dependencies": { "reflect-metadata": "^0.2.2" }, "devDependencies": { "typescript": "^5.6.2" }
} tsconfig.json:
{ "compilerOptions": { "target": "ESNext", "module": "ESNext", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "experimentalDecorators": true, "emitDecoratorMetadata": true }, "include": ["src/**/*"], "exclude": ["node_modules"]
} Steps to Run:
npm install.npm run build.npm start.Output:
Registering LoggerService with key: logger
Registering User with key: user
Executing greet with args: kristal
Log: Greeting kristal
Result of greet: Hello, kristal!
Hello, kristal! Description:
Injectable registers classes in a DI container.Inject uses metadata for dependency injection.LogExecution logs method calls and results.ValidateAge enforces valid age values.RequiredParam ensures required parameters.reflect-metadata for runtime dependency resolution.experimentalDecorators in tsconfig.json.reflect-metadata for metadata-related decorators.descriptor.value.experimentalDecorators and emitDecoratorMetadata in tsconfig.json.reflect-metadata for metadata-driven applications.descriptor.value.async/await in decorators.Reflect.defineMetadata.Reflect.getMetadata.tsc with strict mode (strict: true in tsconfig.json).eslint) with TypeScript plugins for consistency.