DEV Community

Cover image for Getting Rid of Boilerplate in Angular: Using TypeScript Decorators
Art Stesh
Art Stesh

Posted on

2

Getting Rid of Boilerplate in Angular: Using TypeScript Decorators

Every Angular developer has wondered at least once: "Why am I writing so much repetitive code?" Dependency injection, recurring logging methods, uniform event handling—all of it feels like an endless battle with boilerplate code. However, Angular has a powerful feature in its arsenal to simplify tasks and automate repetitive actions—TypeScript Decorators.

Decorators are a quick way to add unified functionality to your codebase, making it cleaner, more understandable, and easier to maintain. In this article, we'll explore how decorators can help eliminate repetitive patterns while introducing flexibility and error reduction.

Introduction to TypeScript Decorators

Decorators are functions applied to classes, methods, properties, or parameters. They allow you to modify the behavior of an object or its elements without altering its original source code. Decorators are available in TypeScript thanks to the ES7 standard. In fact, Angular heavily relies on them: @Component, @Injectable, @Input, etc.

Purpose of Decorators

The main goal of decorators is to add new behavior to objects. They eliminate boilerplate code, ensure reusability of functionality, and make the code more readable. Decorators allow you to:

  1. Modify or extend the functionality of classes, properties, methods, and parameters.
  2. Automate everyday tasks, such as:
    • Logging,
    • Validation,
    • Caching,
    • Dependency injection (DI).
  3. Add metadata, like class or method registration.
  4. Simplify API interaction, freeing developers from manual calls.

Example problem:

Suppose you want to log every method call in the application. Instead of adding console.log() into each method, you can use method decorators:

function LogMethod(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log(`Method invoked: ${propertyKey}, arguments: ${JSON.stringify(args)}`);
    return originalMethod.apply(this, args);
  };

  return descriptor;
}

class Example {
  @LogMethod
  doSomething(param: string) {
    console.log("Doing something important...");
  }
}

const instance = new Example();
instance.doSomething("test");
// Console:
// Method invoked: doSomething, arguments: ["test"]
// Doing something important...
Enter fullscreen mode Exit fullscreen mode

How Do Decorators Work?

Decorators are functions that run at runtime. They are called to add or modify the functionality of a class, its methods, properties, or parameters.

Types of Decorators:

  1. Class Decorators: Operate on the class itself.
  2. Property Decorators: Modify properties or fields of the class.
  3. Method Decorators: Allow modification of a method’s behavior.
  4. Parameter Decorators: Process method or constructor parameters.

Example Tasks & Implementations

Task 1: Method Call Logging (Method Decorator)

Tracking user interactions and actions in an application is a common requirement. Instead of manually adding logger calls in every method, you can automate logging using method decorators.

Implementation:

We create a @LogMethod decorator to log method names and passed arguments:

function LogMethod(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log(`Method invoked: ${propertyKey} with arguments: ${JSON.stringify(args)}`);
    const result = original.apply(this, args);
    console.log(`Method ${propertyKey} returned: ${JSON.stringify(result)}`);
    return result;
  };

  return descriptor;
}
Enter fullscreen mode Exit fullscreen mode

Usage:

class Calculator {
  @LogMethod
  add(a: number, b: number): number {
    return a + b;
  }
}

const calc = new Calculator();
calc.add(5, 7);
Enter fullscreen mode Exit fullscreen mode

Console output:

Method invoked: add with arguments: [5,7]
Method add returned: 12
Enter fullscreen mode Exit fullscreen mode

Task 2: Transformations and Validations (Property Decorator)

In form-based applications, user input often requires automatic transformations and validations. By using property decorators, you can easily add such functionality without overriding set methods explicitly.

Automatic Transformation with @Capitalize:

This decorator ensures string inputs are capitalized:

function Capitalize(target: Object, propertyKey: string) {
  let value: string;

  const getter = () => value;
  const setter = (newValue: string) => {
    value = newValue.charAt(0).toUpperCase() + newValue.slice(1);
  };

  Object.defineProperty(target, propertyKey, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true
  });
}
Enter fullscreen mode Exit fullscreen mode

Usage:

class User {
  @Capitalize
  name: string;

  constructor(name: string) {
    this.name = name;
  }
}

const user = new User("john");
console.log(user.name); // "John"
Enter fullscreen mode Exit fullscreen mode

Input Validation:

Automatically validate inputs with a decorator:

function ValidatePositive(target: Object, propertyKey: string) {
  let value: number;

  const getter = () => value;
  const setter = (newValue: number) => {
    if (newValue < 0) {
      throw new Error(`Property ${propertyKey} must be positive`);
    }
    value = newValue;
  };

  Object.defineProperty(target, propertyKey, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true
  });
}
Enter fullscreen mode Exit fullscreen mode

Usage:

class Product {
  @ValidatePositive
  price: number;

  constructor(price: number) {
    this.price = price;
  }
}

const product = new Product(50);
product.price = -10; // Error: "Property price must be positive"
Enter fullscreen mode Exit fullscreen mode

Task 3: Automating DI in Services (Class Decorator)

Centralize recurring logic for requests, caching, or error handling in Angular services with decorators. Here's an example of a caching decorator @Cacheable:

const methodCache = new Map();

function Cacheable(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;

  descriptor.value = function (...args: any[]) {
    const key = JSON.stringify(args);
    if (methodCache.has(key)) {
      console.log(`Using cache for: ${propertyKey}(${key})`);
      return methodCache.get(key);
    }

    const result = original.apply(this, args);
    methodCache.set(key, result);
    return result;
  };

  return descriptor;
}
Enter fullscreen mode Exit fullscreen mode

Usage:

class ApiService {
  @Cacheable
  fetchData(url: string) {
    console.log(`Fetching data from ${url}`);
    return `Data from ${url}`;
  }
}

const api = new ApiService();
console.log(api.fetchData("https://example.com/api")); // "Fetching data..."
console.log(api.fetchData("https://example.com/api")); // "Using cache..."
Enter fullscreen mode Exit fullscreen mode

Task 4: Improve Angular Components with Decorators

Automate subscriptions in components with @Autounsubscribe to avoid memory leaks.

function AutoUnsubscribe(constructor: Function) {
  const originalOnDestroy = constructor.prototype.ngOnDestroy;

  constructor.prototype.ngOnDestroy = function () {
    for (const prop in this) {
      if (this[prop] && typeof this[prop].unsubscribe === "function") {
        this[prop].unsubscribe();
      }
    }
    if (originalOnDestroy) {
      originalOnDestroy.apply(this);
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

Usage:

@AutoUnsubscribe
@Component({ selector: 'app-example', template: '' })
export class ExampleComponent implements OnDestroy {
  subscription = this.someService.data$.subscribe();

  constructor(private someService: SomeService) {}

  ngOnDestroy() {
    console.log("Component destroyed");
  }
}
Enter fullscreen mode Exit fullscreen mode

Downsides of Decorators and When to Avoid Using Them

While powerful and convenient, decorators are not without drawbacks. There are scenarios where their usage can lead to problems, increased code complexity, or degraded performance.


1. Unstable Standardization

The Problem:

Decorators are still at Stage 2 in the ECMAScript specification. This means their behavior may change, and their implementation could differ in future versions of JavaScript. As a result, the code written with decorators today might require rewriting in the future.

Consequences:

  • Complete reliance on the TypeScript implementation of decorators.
  • Not all JavaScript-compatible tools and libraries support them (e.g., some specific libraries or execution environments).

2. Reduced Readability of Complex Code

The Problem:

Decorators abstract functionality, hiding it behind a concise syntax. As a result, if you use multiple decorators in a single class or component, the program's behavior becomes less predictable—especially for developers unfamiliar with advanced decorators in your project.

Example:

@Auth('admin')
@TrackUsage('createUser')
@Retry(3)
class UserService {
  createUser(user: User) {
    // Implementation
  }
}
Enter fullscreen mode Exit fullscreen mode

For a developer encountering this code, it will be unclear:

  • What each decorator does.
  • How they interact with each other.
  • At what stage of execution each decorator is applied.

When This Becomes Critical:

  1. In projects with a large number of contributors, where code simplicity is paramount.
  2. For newcomers to the project, who may spend extra time understanding the logic.

3. Excessive Magic (Overhead)

The Problem:

Decorators often introduce additional "magical" functionality, leading to unexpected effects. For example, changes via Object.defineProperty or method rewriting can make debugging and understanding the code more challenging.

Consequences:

  • Difficult debugging: The program's behavior may depend on the sequence in which the decorators are applied.
  • Unpredictable bugs: Migrating to a new version of TypeScript or Angular could break the functionality of decorators.

4. Challenges in Testing

The Problem:

Testing tools may struggle to interpret code with excessive hidden logic in decorators. Decorators may introduce proxy layers or change the "underlying" code, making test simulation more complex.

Example:

If you use validation decorators (e.g., @Validate), tests may require direct access to the internal implementation of the decorator, making the test-writing process less straightforward.


5. Debugging Complexity

The Problem:

When decorators not only add metadata but also modify methods/properties, the results can become harder to anticipate. Debugging tools (like debuggers) sometimes display the "unmodified" code, rather than its actual behavior.

Example:

While using a debugger to view a modified method, it might not be immediately clear where and how the decorator has been applied.


When Should You Avoid Using Decorators?

1. Small Projects

In small applications with minimal repetition, decorators are unlikely to justify their added complexity. Additional abstraction will only make the code harder to read, while simplifying minimal logic won’t yield significant benefits.

2. Projects with a Low Learning Curve

If your project is aimed at junior developers or those unfamiliar with TypeScript decorators, their use could become a barrier. For such teams, it's better to avoid overcomplicating code.

3. Risky Modifications to State

If a decorator modifies an existing method/property, it might cause unstable behavior—especially if decorators are layered on top of each other.


Best Practices for Using Decorators

  1. Use Decorators for Repetitive, "Boring" Logic:

    Decorators work best for tasks like logging, authorization, and caching—situations where reducing boilerplate is important.

  2. Avoid Overcomplicating Code with Abstractions:

    If a task can be accomplished with two lines of code without a decorator, it’s better to avoid them.

  3. Document Decorator Behavior:

    Always explain what a decorator does in comments or documentation to avoid confusion.

  4. Monitor Performance:

    If a decorator performs resource-intensive tasks, ensure it doesn’t noticeably impact application performance.

  5. Don’t Add Business Logic to Decorators:

    Decorators should remain lightweight, addressing infrastructure concerns such as logging or validation, not directly processing business data.


Conclusion

Decorators in TypeScript are a powerful tool, but like any tool, they require a reasonable approach. Overusing them, particularly in simple projects, will only complicate the code and make your work harder. It’s important to understand that decorators are best suited for managing cross-cutting concerns (e.g., logging, authorization, validation), but not everything.

Remember: The simpler and clearer the code, the better for you and your team. Decorators are a tool—not a magical solution to all problems! 😊

Top comments (0)