Design Pattern: Decorator Pattern in TypeScript

Decorator pattern adds new functionality to existing implementation, without changing the existing implementation. The additional functionality is added to individual objects (not to the entire class).

This article demonstrates Decorator pattern implementations in TypeScript. Check the implementation steps and examples.

Implementation

Here are the steps to implement Decorator pattern-

Pre-existing implementations-

We need to have subject classes, and an interface that the class implements.

We want to add new functionality to this class and all classes that implement the interface.

Decorator implementation-

  • Create an abstract class, which will be extended by all the concrete decorator.
  • Implement the subject interface, for this abstract decorator class.
  • Create a protected property in the decorator, of the type of the subject interface. This will hold a reference to an object of type subject.
  • In the decorator constructor, accept a subject type object as param, and set that subject to property.
  • In the method implementations, call methods from the subject property.
  • Create concrete decorator classes, and extend the abstract decorator. 

While using the operation, we can create a new subject type object, then pass that object to the decorator, which initializing the decorator.

Here is a simple example of Decorator pattern implementation-

// Decorator pattern implementation in TypeScript

// Subject
interface Subject {
    operationOne(): void;
    operationTwo(): void;
}

// Concrete subject
class ConcreteSubject implements Subject {
    operationOne(): void {
        console.log("Performing Operation One(1) in Subject");
    }

    operationTwo(): void {
        console.log("Performing Operation Two(2) in Subject");
    }
}

// Decorator
abstract class Decorator implements Subject {
    protected subject: Subject;

    constructor(subject: Subject) {
        this.subject = subject;
    }

    operationOne(): void {
        this.subject.operationOne();
    }

    operationTwo(): void {
        this.subject.operationTwo();
    }
}

// Concrete decorator
class ConcreteDecorator extends Decorator {
    constructor(subject: Subject) {
        super(subject);
    }

    public operationOne(): void {
        this.subject.operationOne();
        console.log("Performing additional operation in Concrete Decorator");
    }
}


// Demo
const someDecorator = new ConcreteDecorator(new ConcreteSubject());
someDecorator.operationOne();

Output of the above code will be as below-

Performing Operation One(1) in Subject
Performing additional operation in Concrete Decorator

Examples

Here are a few examples of Decorator pattern implementation in TypeScript-

Example #1: Data Export

Here we have a simple data importer implementation. This exporter is used to export data, with no formatting or process, just simple export.

We want to add export to specific formats like – CSV, JSON, Excel, etc. That’s why we will create a decorator for each type of format.

DataExport Interface [Existing]

  • Create file “data-export.ts”.
  • Create interface “DataExport”.
  • Declare some method, here we have method – “processData”.
// data-export.ts

interface DataExport {
    processData(): void;
}

export default DataExport;

SimpleExport Class [Existing]

  • Create file “simple-data-export.ts”.
  • Create class “SimpleDataExport”.
  • Implement interface “DataExport”. Define “processData” method for the class.
// simple-data-export.ts

import DataExport from "./data-export";


class SimpleDataExport implements DataExport {
    processData(): void {
        console.log("Processing Data");
    }
}

export default SimpleDataExport;

Main Decorator Class [Decorator]

  • Create file “data-export-decorator.ts”.
  • Create class “DataExportDecorator”.
  • Declare a property “dataExporter” of type “DataExport”.
  • Define constructor, that accepts parameter of type “DataExport” and sets that to the “dataExport”.
  • Implement interface “DataExport”. Define the method “processData” for interface implementation.
// data-export-decorator.ts

import DataExport from "./data-export";


class DataExportDecorator implements DataExport {
    protected dataExporter: DataExport;

    constructor(dataExporter: DataExport) {
        this.dataExporter = dataExporter;
    }

    public processData() {
        this.dataExporter.processData();
    }
}

export default DataExportDecorator;

CSV Decorator Class [Decorator]

  • Create file “csv-data-export-decorator.ts”.
  • Create class “CsvDataExportDecorator”.
  • Extend the main decorator “DataExportDecorator”.
  • Define method “processCsv”. This method will perform the data format conversion to CSV. 
  • Define method “processData”. Call the “processData” method from the “dataExport” object, and also call the “processCsv” in this method.
// csv-data-export-decorator.ts

import DataExport from "./data-export";
import DataExportDecorator from "./data-export-decorator";

class CsvDataExportDecorator extends DataExportDecorator {
    constructor(dataExporter: DataExport) {
        super(dataExporter);
    }

    processData(): void {
        this.dataExporter.processData();
        this.processCsv();
    }

    processCsv(): void {
        console.log("Processed data to CSV");
    }
}

export default CsvDataExportDecorator;

Excel Decorator Class [Decorator]

  • Create file “excel-data-export-decorator.ts”.
  • Create class “ExcelDataExportDecorator”.
  • Extend “DataExportDecorator” for this class.
  • Define method “processExcel”. This method will perform the data format conversion to Excel. 
  • Define method “processData”. Call the “processData” method from the “dataExport” object, and also call the “processExcel” in this method.
// excel-data-export-decorator.ts

import DataExport from "./data-export";
import DataExportDecorator from "./data-export-decorator";

class ExcelDataExportDecorator extends DataExportDecorator {
    constructor(dataExporter: DataExport) {
        super(dataExporter);
    }

    processData(): void {
        this.dataExporter.processData();
        this.processExcel();
    }

    processExcel() {
        console.log("Processed data to Excel");
    }
}

export default ExcelDataExportDecorator;

JSON Decorator Class [Decorator]

  • Create file “json-data-export-decorator.ts”.
  • Create class “JsonDataExportDecorator”.
  • Extend “DataExportDecorator” for this class.
  • Define method “processJson”. This method will perform the data format conversion to JSON. 
  • Define method “processData”. Call the “processData” method from the “dataExport” object, and also call the “processJson” in this method.
// json-data-export-decorator.ts

import DataExport from "./data-export";
import DataExportDecorator from "./data-export-decorator";


class JsonDataExportDecorator extends DataExportDecorator {
    constructor(dataExporter: DataExport) {
        super(dataExporter);
    }

    processData(): void {
        this.dataExporter.processData();
        this.processJson();
    }

    processJson(): void {
        console.log("Processed data to JSON");
    }
}

export default JsonDataExportDecorator;

Demo

To use any specific operator pass an object of “SimpleDataExport” to the decorator.

// demo.ts

import CsvDataExportDecorator from "./csv-data-export-decoraor";
import ExcelDataExportDecorator from "./excel-data-export-decorator";
import JsonDataExportDecorator from "./json-data-export-decorator";
import SimpleDataExport from "./simple-data-export";

const csvDataExport = new CsvDataExportDecorator(new SimpleDataExport());
csvDataExport.processData();

const excelDataExport = new ExcelDataExportDecorator(new SimpleDataExport());
excelDataExport.processData();

const jsonDataExport = new JsonDataExportDecorator(new SimpleDataExport());
jsonDataExport.processData();

Output

The output of the demo above will be like below.

Processing Data
Processed data to CSV

Processing Data
Processed data to Excel

Processing Data
Processed data to JSON

Example #2: UI Elements

In this example we have some UI elements, like – Button, Input Box, and Table. We want to implement some decorators, like – margin, border, and background.

UI Element Interface

  • Create file “ui-element.ts”.
  • Create interface “UIElement”.
  • Declare method “draw”.
// ui-element.ts

interface UIElement {
    draw(): void;
}

export default UIElement;

Button UI Element Class

  • Create file “button.ts”.
  • Create class “Button”.
  • Implement interface “UIElement”, define the method “draw”.
// button.ts

import UIElement from "./ui-element";

class Button implements UIElement {
    draw(): void {
        console.log("Drawing Button");
    }
}

export default Button;

Input Box UI Element Class

  • Create file “input-box.ts”.
  • Create class “InputBox”.
  • Implement interface “UIElement” for the class.
// input-box.ts

import UIElement from "./ui-element";

class InputBox implements UIElement {
    draw(): void {
        console.log("Drawing Input Box");
    }
}

export default InputBox;

Table UI Element Class

  • Create file “table.ts”.
  • Create class “Table”.
  • Implement interface “UIElement”.
// table.ts

import UIElement from "./ui-element";

class Table implements UIElement {
    draw(): void {
        console.log("Drawing Table");
    }
}

export default Table;

Abstract UI Decorator [Decorator]

  • Create file “ui-decorator.ts”.
  • Define the class “UIDecorator”.
  • Declare a property “uiElement” of type “UIElement”.
  • In the constructor accept an object of type “UIElement”, and set the value to “uiElement”.
  • Implement interface “UIElement” for the class. Define the method “draw”, and call the “draw” method from “uiElement” in this method implementation.
// ui-decorator.ts

import UIElement from "./ui-element";

abstract class UIDecorator implements UIElement {
    protected readonly uiElement: UIElement;

    protected constructor(uiElement: UIElement) {
        this.uiElement = uiElement;
    }

    draw(): void {
        this.uiElement.draw();
    }
}

export default UIDecorator;

Border Decorator Class [Decorator]

  • Create file “border-decorator.ts”.
  • Define the class “BorderDecorator”.
  • Extend the decorator class “UIDecorator”.
  • In the constructor accept an object of type “UIElement”, and pass that to the parent constructor using “super”.
  • Define the method “draw”. Call the “draw” method from the decorator and add the implementation for adding the border.
// border-decorator.ts

import UIDecorator from "./ui-decorator";
import UIElement from "./ui-element";

class BorderDecorator extends UIDecorator {
    constructor(uiElement: UIElement) {
        super(uiElement);
    }

    draw(): void {
        super.draw();
        console.log("Adding Border to the element");
    }
}

export default BorderDecorator;

Background Decorator Class [Decorator]

  • Create file “background-decorator.ts”.
  • Define the class “BackgroundDecorator”.
  • Extend the decorator class “UIDecorator”.
  • In the constructor and pass “UIElement” to the parent constructor using “super”.
  • Define the method “draw”. Call the “draw” method from the decorator and add the implementation for adding the background.
// background-decorator.ts

import UIDecorator from "./ui-decorator";
import UIElement from "./ui-element";


class BackgroundDecorator extends UIDecorator {
    constructor(uiElement: UIElement) {
        super(uiElement);
    }

    draw(): void {
        super.draw();
        console.log("Adding Background to the element");
    }
}

export default BackgroundDecorator;

Margin Decorator Class [Decorator]

  • Create file “margin-decorator.ts”.
  • Define the class “MarginDecorator”.
  • Extend class “UIDecorator”.
  • In the constructor and pass “UIElement” to the parent constructor using “super”.
  • Define the method “draw”. Call the “draw” method from the decorator and add the implementation for adding the margin.
// margin-decorator.ts

import UIDecorator from "./ui-decorator";
import UIElement from "./ui-element";

class MarginDecorator extends UIDecorator {
    constructor(uiElement: UIElement) {
        super(uiElement);
    }

    public draw() {
        super.draw();
        console.log("Adding margin to the element");
    }
}

export default MarginDecorator;

Demo

The following code demonstrates how we can use multiple decorators on a single UI Element, by chaining the decorator calls.

// demo.ts

import BackgroundDecorator from "./background-decorator";
import BorderDecorator from "./border-decorator";
import Button from "./button";
import InputBox from "./input-box";
import MarginDecorator from "./margin-decorator";
import Table from "./table";

const tableWithBorder = new BorderDecorator(new Table());
tableWithBorder.draw();

const inputWithBorderAndBackground = new BackgroundDecorator(new BorderDecorator(new InputBox()));
inputWithBorderAndBackground.draw();

const buttonWithAllDecorator = new MarginDecorator(new BackgroundDecorator(new BorderDecorator(new Button())));
buttonWithAllDecorator.draw();

Output

Drawing Table
Adding Border to the element

Drawing Input Box
Adding Border to the element
Adding Background to the element

Drawing Button
Adding Border to the element
Adding Background to the element
Adding margin to the element

Source Code

Use the following link to get the source code:

Other Code Implementations

Use the following links to check Decorator pattern implementation in other programming languages.

Leave a Comment


The reCAPTCHA verification period has expired. Please reload the page.