TypeScript Decorators - Advanced Patterns and Best Practices

TypeScript Decorators: Advanced Patterns and Best Practices

In A Practical Guide to TypeScript Decorators, we covered the fundamentals of class and method decorators. In this follow-up, we’ll explore advanced patterns, composition techniques, and production-ready best practices for using decorators in TypeScript applications.

Composing Decorators

You can create powerful combinations by composing multiple decorators:

function ApiEndpoint(config: {
  method: string;
  path: string;
  roles?: string[];
  rateLimit?: number;
  cache?: boolean;
}) {
  return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
    // Apply multiple decorators programmatically
    Route(config.method, config.path)(target, propertyName, descriptor);
    
    if (config.roles) {
      Authorize(config.roles)(target, propertyName, descriptor);
    }
    
    if (config.rateLimit) {
      RateLimit(config.rateLimit)(target, propertyName, descriptor);
    }
    
    if (config.cache) {
      Memoize(target, propertyName, descriptor);
    }
  };
}

class ProductController {
  @ApiEndpoint({
    method: 'GET',
    path: '/products',
    rateLimit: 100,
    cache: true
  })
  getProducts(req: any, res: any) {
    // Handler implementation
  }
  
  @ApiEndpoint({
    method: 'POST',
    path: '/products',
    roles: ['admin'],
    rateLimit: 10
  })
  createProduct(req: any, res: any) {
    // Handler implementation
  }
}

TypeScript Configuration

To use decorators, ensure your tsconfig.json is properly configured:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "target": "ES2020",
    "lib": ["ES2020"],
    "strict": true
  }
}

You’ll also need to install the reflect-metadata library:

npm install reflect-metadata

And import it at the top of your main file:

import 'reflect-metadata';

Production Best Practices

When using decorators in production applications:

  1. Performance: Decorators add runtime overhead. Use them judiciously for cross-cutting concerns, not core business logic.

  2. Testing: Mock or spy on decorated methods carefully. Consider creating decorator-free versions for unit tests.

  3. Bundle Size: Decorators can increase bundle size. Tree-shake unused decorators and consider lazy loading for complex decorator logic.

  4. Type Safety: Use proper TypeScript types for your decorators to maintain type safety:

type MethodDecorator<T = any> = (
  target: any,
  propertyKey: string | symbol,
  descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;

Conclusion

Decorators provide a powerful way to implement cross-cutting concerns in TypeScript applications. They shine in scenarios involving metadata, aspect-oriented programming, and framework development. While they add complexity, the benefits of cleaner, more maintainable code often outweigh the costs.

As you continue building enterprise TypeScript applications, consider decorators for logging, caching, validation, authorization, and other concerns that span multiple parts of your application. They’re particularly valuable in API development, where they can significantly reduce boilerplate while improving code organization.

The key is to use them thoughtfully—decorators should enhance your code’s readability and maintainability, not obscure its intent. Start with simple use cases and gradually build more sophisticated patterns as your team becomes comfortable with the concept.

Remember that decorators are still evolving in both TypeScript and the ECMAScript specification, so stay updated with the latest developments and best practices in the community.