Beyond Basics: Designing Flexible and Extensible TypeScript Applications with Decorator Design Pattern
Tags: typescript, designpatterns, programming, architecture
Published: 2023-03-19 14:51:43

The Decorator design pattern is a structural pattern that allows behavior to be added to an individual object at runtime without affecting the behavior of other objects from the same class. This blog post will explore how to use the Decorator pattern in Typescript and its benefits and drawbacks.

#### Note about the Decorators feature in Typescript</summary>

- The Decorator design pattern and the <a target="_blank" rel="noopener noreferrer nofollow" href="https://www.typescriptlang.org/docs/handbook/decorators.html" style="pointer-events: none">Decorators</a> feature in TypeScript are related concepts, but they have some key differences. The <a target="_blank" rel="noopener noreferrer nofollow" href="https://www.typescriptlang.org/docs/handbook/decorators.html" style="pointer-events: none">Decorators</a> is a TypeScript feature that allows you to annotate classes and class members with decorator functions. These decorators are functions that can modify the behavior of a class or its members. One key difference between them is that the Decorator pattern is a well-established software design pattern that can be used in any programming language, while <a target="_blank" rel="noopener noreferrer nofollow" href="https://www.typescriptlang.org/docs/handbook/decorators.html" style="pointer-events: none">Decorators</a> is a specific (experimental) feature of TypeScript.

## Implementing the Decorator

Let's consider a simple example where we have a class called `Shape` with a method called `draw`. We want to add a border to the shape without changing the original code. We can achieve this using the Decorator pattern.

Let's start with an `IShape` interface and the `Shape` class that implements it.

```typescript
interface IShape {
  draw(): void;
}

class Shape implements IShape {
  public draw(): void {
    console.log('Drawing a shape');
  }
}
```

Now, let's create a `ShapeWithBorder` class that will decorate the `Shape` class and will add a border to the shape. As the first step, the `ShapeWithBorder` class will implement the `IShape` interface.

```typescript
class ShapeWithBorder implements IShape {
  private shape: IShape;

  constructor(shape: IShape) {
    this.shape = shape;
  }

  public draw(): void {
    console.log('Adding a border to the shape');
    this.shape.draw();
  }
}
```

As the second step, the `ShapeWithBorder` class accepts an object implementing the `IShape` interface in its constructor. This way we can provide any `IShape` compliant object to it, and the class will decorate this object with a border. Notice that because `ShapeWithBorder` implements `IShape` can also act as a decoration receiver. This way, we can add multiple decorators on top of each other, which we will explore in the next section.

Now, we can create an instance of the `ShapeWithBorder` class and pass an instance of the `Shape` class to its constructor. Finally, we call the `draw` method on the `ShapeWithBorder` instance, which will add a border to the shape and draw it.

```typescript
const shape = new ShapeWithBorder(new Shape());

shape.draw();
```

You can find this example in this [CodeSandbox](https://codesandbox.io/p/sandbox/beyond-basics-typescript-decorator-pattern-forked-mzblfi?file=%2Fsrc%2Findex.ts).

## Beyond Basics

Imagine you're building a system to generate reports for a finance company. You have a `Report` class that generates a basic report, but you want to add additional features to the report based on the user's preferences. For example, some users may want to see charts and graphs, while others may want to see tables of data. Instead of creating separate classes for each report type or using inheritance, you can use the Decorator pattern to add the required features at runtime.

First, let's define an interface for our `Report` class:

```typescript
interface IReport {
  getData(): Record<number, number>;
  generate(): void;
}
```

Next, let's create a basic `Report` class that implements the interface:

```typescript
class BasicReport implements IReport {
  constructor(private _data: Record<number, number>) {}

  public getData(): Record<number, number> {
    return this._data;
  }

  public generate(): void {
    console.log('Generating basic report...');
  }
}
```

Now, let's create two decorator classes, one for charts and one for tables:

```typescript
class ReportWithCharts implements IReport {
  private report: IReport;

  constructor(report: IReport) {
    this.report = report;
  }

  public getData(): Record<number, number> {
    return this.report.getData();
  }

  public generate(): void {
    this.report.generate();

    console.log('Adding charts to report...');
    const data = this.report.getData();
    // Use data to generate charts...
  }
}

class ReportWithTables implements IReport {
  private report: IReport;

  constructor(report: IReport) {
    this.report = report;
  }

  public getData(): Record<number, number> {
    return this.report.getData();
  }

  public generate(): void {
    this.report.generate();

    console.log('Adding tables to report...');
    const data = this.report.getData();
    // Use data to generate the tables...
  }
}
```

Finally, we can create an instance of the `BasicReport` class and pass it through the decorator classes to add the required features:

```typescript
const now = Date.now()
const sampleData = {
  [now-2]: 123,
  [now-1]: 456,
  [now]: 789,
}

const report: IReport = new ReportWithCharts(
  new ReportWithTables(new BasicReport(sampleData))
);

report.generate();
```

You can find the full example in this [CodeSandbox](https://codesandbox.io/p/sandbox/beyond-basics-typescript-decorator-pattern-hokngu?file=%2Fsrc%2Findex.ts).

This will generate a report with charts and tables, but you can easily modify it to generate a report with only charts or only tables by removing the corresponding decorator classes.

We can also create new decorator classes for additional features without modifying the existing code, which makes it easier to maintain and extend the system over time.

### Benefits

1. **Flexibility**: The pattern provides a flexible way to add behavior to an object at runtime. It allows you to add or remove behavior without changing the original code, making it easier to maintain.
    
2. **Reusability**: Decorators can be reused on different objects to add the same behavior, making the code more efficient and reducing the need to write duplicate code.
    
3. **Separation of concerns**: Separating an advanced or optional behavior of an object from its core functionality.
    

### Drawbacks

1. **Complexity**: Using the Decorator pattern can make the code more complex, especially when dealing with multiple decorators.
    
2. **Performance**: Adding behavior at runtime can affect the performance of your code. It all depends on how the decorators and the core class are implemented. It is important to avoid the N+1 problem, which can be created by executing the core class for each decorator that encapsulates it within the single chain.
    

In conclusion, the Decorator pattern is a valuable design pattern that can be used in Typescript to add behavior to an object at runtime without affecting other objects of the same class. It provides flexibility, reusability, and separation of concerns. However, it can also make the code more complex and affect performance. By following the examples outlined in this blog post, you can effectively implement the Decorator pattern in your Typescript code.


Photo by <a href="https://unsplash.com/pt-br/@martinsanchez?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Martin Sanchez</a> on <a href="https://unsplash.com/photos/NfLZeAN7I6s?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>