TypeScript supports five types of decorators: 1) Class decorators (@classDecorator), 2) Method decorators (@methodDecorator), 3) Property decorators (@propertyDecorator), 4) Accessor decorators (@accessorDecorator), and 5) Parameter decorators (@parameterDecorator). Each type receives different arguments and can be used for different purposes.
reflect-metadata is a library that adds a polyfill for the Metadata Reflection API. It's used with decorators to add and read metadata about classes, methods, and properties. Enable with 'emitDecoratorMetadata: true' in tsconfig.json. Example: import 'reflect-metadata'; @Reflect.metadata('role', 'admin') class User {}
Lazy initialization decorators delay property initialization until first access. Example: function lazy<T>(initializer: () => T) { return function(target: any, key: string) { let value: T; Object.defineProperty(target, key, { get: () => { if (!value) value = initializer(); return value; } }); }; } class Example { @lazy(() => expensiveOperation()) value: string; }
Decorators are special declarations that can be attached to class declarations, methods, properties, or parameters. They are enabled by setting 'experimentalDecorators: true' in tsconfig.json. Example: @decorator class Example {}. Decorators provide a way to add annotations and metadata to existing code.
Class decorators are declared before a class declaration and receive the constructor as an argument. Example: function logger(target: Function) { console.log(`Creating class: ${target.name}`); } @logger class Example {}. They can modify or replace the class definition and are useful for adding metadata or behavior to classes.
Decorator factories are functions that return decorators and allow customization through parameters. Example: function log(prefix: string) { return function(target: any) { console.log(`${prefix}: ${target.name}`); }; } @log('MyApp') class Example {}. They provide a way to customize decorator behavior.
Parameter decorators are applied to method parameters and receive three arguments: target (prototype), methodName, and parameterIndex. Example: function required(target: Object, propertyKey: string, parameterIndex: number) { const requiredParams = Reflect.getMetadata('required', target, propertyKey) || []; requiredParams.push(parameterIndex); Reflect.defineMetadata('required', requiredParams, target, propertyKey); }
Common use cases include: 1) Logging and monitoring, 2) Property validation, 3) Dependency injection, 4) Method memoization, 5) Access control and authorization, 6) Class and property transformation, 7) API endpoint definition (e.g., in NestJS), 8) Observable properties (e.g., in Angular), 9) Type serialization/deserialization.
Decorators can implement dependency injection by using metadata to store and retrieve dependencies. Example: function Injectable() { return function(target: any) { Reflect.defineMetadata('injectable', true, target); }; } @Injectable() class Service {} This pattern is used in frameworks like Angular and NestJS.
Key differences include: 1) Syntax variations, 2) TypeScript decorators are experimental while ES decorators are part of the standard, 3) Different metadata handling, 4) ES decorators have additional capabilities like decorating object literals, 5) TypeScript decorators may need updates to align with ES decorator standard when finalized.
Accessor decorators are applied to getter/setter declarations and receive three arguments similar to method decorators. Example: function enumerable(value: boolean) { return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) { descriptor.enumerable = value; }; } They're useful for modifying accessor behavior.
Multiple decorators can be applied to a declaration in sequence. They are evaluated in reverse order (bottom to top). Example: @f @g class C {}. Evaluation order: g then f. For decorator factories: @f() @g() class C {}, the factories are evaluated in order (f then g) but the decorators in reverse order.
Memoization decorators cache function results based on arguments. Example: function memoize(target: any, key: string, descriptor: PropertyDescriptor) { const original = descriptor.value; const cache = new Map(); descriptor.value = function(...args: any[]) { const key = JSON.stringify(args); if (cache.has(key)) return cache.get(key); const result = original.apply(this, args); cache.set(key, result); return result; }; }
Design-time type metadata is automatically generated when emitDecoratorMetadata is enabled. It includes type information for parameters, return types, and properties. Example: const paramTypes = Reflect.getMetadata('design:paramtypes', target, key); This is used by frameworks for dependency injection and validation.
Best practices include: 1) Use decorator factories for configuration, 2) Keep decorators focused and single-purpose, 3) Handle errors gracefully, 4) Use meaningful names that describe behavior, 5) Document decorator requirements and effects, 6) Avoid side effects in decorator evaluation, 7) Use metadata for storing configuration, 8) Consider performance implications.
Method decorators are declared before method declarations and receive three arguments: target (prototype), propertyKey (method name), and descriptor (property descriptor). Example: function log(target: any, key: string, descriptor: PropertyDescriptor) { // Original method const original = descriptor.value; descriptor.value = function(...args: any[]) { console.log(`Calling ${key}`); return original.apply(this, args); }; }
Property decorators receive two arguments: target (prototype) and propertyKey (property name), unlike method decorators which also receive a descriptor. Example: function validate(target: any, key: string) { let value = target[key]; Object.defineProperty(target, key, { get: () => value, set: (newValue) => { if (!newValue) throw new Error('Value cannot be null'); value = newValue; } }); }
Metadata allows storing additional information about classes, methods, and properties that can be accessed at runtime. It's enabled with reflect-metadata. Example: Reflect.defineMetadata('validation', { required: true }, target, propertyKey); const metadata = Reflect.getMetadata('validation', target, propertyKey);
Validation decorators can be implemented using property or parameter decorators with metadata. Example: function required(target: any, propertyKey: string) { const validationMetadata = { required: true }; Reflect.defineMetadata('validation', validationMetadata, target, propertyKey); } class User { @required name: string; }
Limitations include: 1) Experimental feature requiring compiler flag, 2) Can't decorate declarations in .d.ts files, 3) Limited to specific declaration types (class, method, property, parameter), 4) Can't access decorated values during declaration phase, 5) No direct way to decorate local variables or function declarations.
Method override decorators can verify proper method overriding. Example: function override(target: any, key: string, descriptor: PropertyDescriptor) { const baseClass = Object.getPrototypeOf(target); if (!baseClass[key]) throw new Error(`${key} doesn't override any base method`); return descriptor; } class Child extends Parent { @override method() {} }
Async operations in decorators require special handling since decorators run during class definition. Example: function asyncInit() { return function(target: any) { return class extends target { async init() { await super.init(); // Additional async initialization } }; }; } @asyncInit() class Service { async init() { /* ... */ } }