Samuel Fajreldines

I am a specialist in the entire JavaScript and TypeScript ecosystem.

I am expert in AI and in creating AI integrated solutions.

I am expert in DevOps and Serverless Architecture

I am expert in PHP and its frameworks.

+55 (51) 99226-5039 samuelfajreldines@gmail.com

Mastering TypeScript Decorators: How to Use and How to Create

TypeScript decorators are a powerful feature that can transform the way code is organized, annotated, and maintained. By attaching special behavior to classes, methods, accessors, properties, or parameters, decorators dramatically improve readability and structure in large-scale applications. While decorators are widely used in frameworks like Angular, they can also be employed in other contexts, from Node.js backends to library development. This comprehensive guide explores how to use and create TypeScript decorators, offering tangible examples and best practices to enhance your development workflow.

Understanding the Fundamentals of TypeScript Decorators

In general programming terms, a decorator is a way to add additional functionality to code without modifying its original structure. In TypeScript, decorators work in a similar fashion by allowing you to apply annotation-based logic to your classes, methods, and more. Each decorator is essentially a function that receives some form of metadata about the target it is decorating (e.g., a class constructor, a method descriptor) and can perform operations like wrapping, modifying, or logging specific behavior.

Why Decorators Matter

  1. Cleaner Code: Instead of stuffing business logic or cross-cutting concerns (such as logging or performance metrics) directly into your class methods, decorators let you annotate these elements with focused code. This approach offers a clear separation of responsibilities and allows code to remain more cohesive.

  2. Reusability: Once a decorator is defined, it can be easily reused throughout an application or shared between multiple projects. For example, you can create a logging decorator and apply it to multiple classes to track performance metrics or log method calls consistently.

  3. Enhanced Readability: Decorators convey intent. When you see a method named @Log() or a class marked with @Controller(), you instantly understand what additional capabilities or transformations are being applied. This leads to more maintainable codebases, especially when working in teams where multiple contributors need to grok the code quickly.

  4. Framework Integration: Angular introduced a mainstream use of TypeScript decorators for denoting components, modules, services, and more. Recognizing their power and readability, other frameworks can take advantage of decorators as well, making them a valuable tool for any TypeScript developer aiming to build robust and scalable applications.

Types of Decorators in TypeScript

TypeScript provides different kinds of decorators, each targeting a specific language feature. Knowing which decorator to employ in each scenario is key to maximizing their benefits:

  1. Class Decorators
    A class decorator is a function that takes a constructor as its parameter. It can create new constructors, perform additional logic, or attach new properties to the class. They are commonly used to mark classes with specific roles (like @Controller()) and to perform any high-level initialization or transformation.

  2. Method Decorators
    A method decorator receives a target (the class prototype), a method name, and a property descriptor. You can use method decorators to define logging, caching, or other cross-cutting concerns that should apply every time a specific method is called. For instance, you might intercept an HTTP call and automatically add headers or handle exceptions.

  3. Accessor Decorators
    Similar to method decorators, accessor decorators intercept property getters and setters. They are often used for data validation, transformations, or logging property changes. While not as frequently used as class or method decorators, they can be invaluable in certain data-centric scenarios.

  4. Property Decorators
    A property decorator receives a target and the name of the decorated property. Though they cannot directly modify property initializers, they can attach metadata to properties. This is especially useful for automatic ORM bindings or serializing class properties to JSON.

  5. Parameter Decorators
    Finally, parameter decorators allow you to annotate parameters within a method’s signature. They are often used in frameworks to inject services or dependencies, giving you the ability to peel away complexities from your method signatures.

Prerequisites for Using Decorators

To use decorators in TypeScript, enable the experimentalDecorators compiler option in your tsconfig.json:

{
  "compilerOptions": {
    "target": "ES6",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    ...
  },
  ...
}
  • experimentalDecorators: Allows the usage of decorators.
  • emitDecoratorMetadata: Emits type metadata, particularly important for more advanced reflection scenarios (for instance, using the reflect-metadata library).

With these configurations set, you are ready to start applying decorators in your TypeScript projects.

How to Use Decorators in Practice

Class Decorator Example

Here is a simple class decorator that adds a timestamp property to track instantiation times:

function Timestamped(constructor: Function) {
  constructor.prototype.createdAt = new Date();
}

@Timestamped
class ReportService {
  constructor() {
    // Some initialization code
  }

  generateReport() {
    console.log("Report generated");
  }
}

const service = new ReportService();
console.log(service["createdAt"]); // A Date object representing creation time

In this example:

  1. Timestamped is a class decorator function.
  2. It modifies the constructor.prototype to include a createdAt property, set to the current time.
  3. Any instance of ReportService will now have a createdAt metadata attached.

Method Decorator Example

Method decorators allow you to intercept and augment function calls. Here’s a simple logging decorator:

function LogMethod(
  target: any,
  propertyName: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;
  
  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyName} with arguments:`, args);
    const result = originalMethod.apply(this, args);
    console.log(`Result of ${propertyName}:`, result);
    return result;
  };

  return descriptor;
}

class TaskService {
  @LogMethod
  runTask(taskId: string) {
    return `Task ${taskId} completed successfully!`;
  }
}

const taskService = new TaskService();
taskService.runTask("1234");

In the above code:

  1. LogMethod intercepts calls to runTask.
  2. Logs the method name and arguments before executing the original runTask.
  3. Logs the method’s result afterward, then returns the result.

This separation of concerns allows you to cleanly handle cross-cutting logic like logging without cluttering core methods.

Property Decorator Example

Property decorators can attach metadata to specific class properties. For instance, imagine an ORM that needs to identify which properties map to database columns:

import "reflect-metadata";

const COLUMN_KEY = Symbol("column");

/**
 * Attach metadata to a property to mark it as a DB column.
 */
function Column(columnName?: string): PropertyDecorator {
  return (target, propertyKey) => {
    Reflect.defineMetadata(COLUMN_KEY, columnName || propertyKey, target, propertyKey);
  };
}

class User {
  @Column("user_id")
  id: number;

  @Column()
  username: string;

  @Column()
  email: string;
}

// Later you can retrieve metadata to build queries or define migrations
function getColumnMetadata(target: any, propertyKey: string) {
  return Reflect.getMetadata(COLUMN_KEY, target, propertyKey);
}

With this pattern:

  1. Each property decorated with @Column() is annotated with metadata specifying the corresponding column name.
  2. The ORM layer can introspect the class to map properties to their respective columns, generating queries dynamically.

This approach highlights the real power of decorators for building flexible, annotation-driven systems.

How to Create Your Own Decorators

Creating new decorators follows patterns similar to those above. You define a function that accepts certain parameters (depending on whether it decorates a class, method, etc.) and then returns a function that targets the code piece you want to augment.

Step-by-Step Example: Custom Validator Decorator

Suppose you want a simple validation system that checks whether an argument passed into a method is not empty. Here’s how you can build a custom method parameter decorator to enforce that:

  1. Create the decorator function:
import "reflect-metadata";

const VALIDATE_KEY = Symbol("validateParams");

function NotEmpty(
  target: Object,
  propertyKey: string | symbol,
  parameterIndex: number
) {
  let existingRequiredParameters: number[] =
    Reflect.getOwnMetadata(VALIDATE_KEY, target, propertyKey) || [];
  existingRequiredParameters.push(parameterIndex);
  Reflect.defineMetadata(
    VALIDATE_KEY,
    existingRequiredParameters,
    target,
    propertyKey
  );
}
  1. Create a helper function to execute validations before calling the original method:
function Validate(
  target: any,
  propertyName: string,
  descriptor: PropertyDescriptor
) {
  const method = descriptor.value;

  descriptor.value = function (...args: any[]) {
    const requiredParameters: number[] =
      Reflect.getOwnMetadata(VALIDATE_KEY, target, propertyName) || [];

    requiredParameters.forEach((index) => {
      if (args[index] === null || args[index] === undefined || args[index] === "") {
        throw new Error(`Validation Error: Parameter at index ${index} is empty`);
      }
    });

    return method.apply(this, args);
  };

  return descriptor;
}
  1. Apply these decorators in a class:
class MailService {
  @Validate
  sendEmail(@NotEmpty recipient: string, message: string) {
    console.log(`Sending email to ${recipient} with message: ${message}`);
  }
}

const mailService = new MailService();
mailService.sendEmail("john@example.com", "Hello John!"); // Works fine
mailService.sendEmail("", "This should fail validation"); // Throws Validation Error

What’s going on here?

  • @NotEmpty is a parameter decorator that records the index of the parameter in the method’s signature.
  • @Validate is a method decorator that retrieves those recorded parameter indexes and checks whether they have valid values before executing the method.
  • If validation fails, an error is thrown. Otherwise, the method proceeds as normal.

This modular approach underscores how decorators can elegantly implement advanced logic in a reusable way.

Decorators in Angular

Angular relies heavily on TypeScript decorators to identify classes as components, directives, pipes, etc. When you see an @Component(...) annotation, it’s effectively a class decorator that extends and annotates the class to be recognized by Angular’s compilation process. Similarly, services use the @Injectable() decorator, which allows them to be injected into different parts of the application.

Despite this heavy usage being somewhat behind-the-scenes, understanding how decorators work under the hood helps in debugging, custom library development, or advanced framework modifications.

Best Practices for Decorators

  1. Keep Them Focused: Decorators should have a single responsibility, such as logging, validation, or transformation. Overloading a decorator with multiple tasks can lead to confusion and complexity.

  2. Limit Shared State: When decorators modify class prototypes or method descriptors, ensure changes are contained and well-documented. Avoid patterns that might mask or overwrite existing functionality without transparency.

  3. Leverage Metadata Wisely: Libraries like reflect-metadata make it easy to store and retrieve annotated data. Use it judiciously to maintain clarity and performance. If excessive metadata is attached, it can become challenging to maintain or reason about.

  4. Combine with Other Patterns: Decorators are an excellent complement to dependency injection, factory methods, or middleware-based designs. For example, you could write a decorator that automatically transforms method responses into HTTP responses, bridging the gap between your domain logic and the web framework.

  5. Test Thoroughly: Because decorators often implement cross-cutting concerns, they can be easy to forget or overlook during testing. Ensure that your unit tests cover both the core functionality and the behavior introduced by decorators.

Moving Forward with TypeScript Decorators

Armed with an understanding of decorators, you can begin applying them to real-world applications. Whether your goal is to streamline logging, enforce validation, or embed domain-specific metadata, decorators offer a clean, modular way to enhance your code. They can dramatically reduce boilerplate, promote consistency, and empower you to shape the behavior of your application in new and exciting ways.

Beyond these basics, there’s plenty more to explore. Advanced topics include:

  • Decorator factories, allowing parameters to be passed to decorators.
  • Applying multiple decorators to a single method or class.
  • Using reflection to integrate with advanced frameworks or build custom frameworks.
  • Performance considerations, especially for large-scale applications with numerous decorators.

By consistently employing well-crafted decorators, you can not only improve the maintainability of your applications but also offer new levels of clarity for your development team. Whether you’re architecting a massive enterprise platform or experimenting with a personal side project, harnessing the full potential of TypeScript decorators will help you craft more flexible, concise, and robust solutions.


Resume

Experience

  • SecurityScoreCard

    Nov. 2023 - Present

    New York, United States

    Senior Software Engineer

    I joined SecurityScorecard, a leading organization with over 400 employees, as a Senior Full Stack Software Engineer. My role spans across developing new systems, maintaining and refactoring legacy solutions, and ensuring they meet the company's high standards of performance, scalability, and reliability.

    I work across the entire stack, contributing to both frontend and backend development while also collaborating directly on infrastructure-related tasks, leveraging cloud computing technologies to optimize and scale our systems. This broad scope of responsibilities allows me to ensure seamless integration between user-facing applications and underlying systems architecture.

    Additionally, I collaborate closely with diverse teams across the organization, aligning technical implementation with strategic business objectives. Through my work, I aim to deliver innovative and robust solutions that enhance SecurityScorecard's offerings and support its mission to provide world-class cybersecurity insights.

    Technologies Used:

    Node.js Terraform React Typescript AWS Playwright and Cypress