Design Pattern: Composite Pattern in TypeScript

Compsite pattern enables a single object and a list of objects to operate the same way. That way the client does not need to know if it is using a single object or a list, while using it. So the usage becomes simple in that case.

This article describes Composite pattern implementation in TypeScript. Check the implementation details.

Implementation

To implement the Composite pattern, make sure the item/leaf classes using the same interface, then we can implement the composite class using the steps below-

  • Create a composite class.
  • Implement the same interface that the items are implementing. That way, the same interface in ensured.
  • Store a list of items in a private property.
  • When performing operations, perform that on the item list in a loop.
  • Implement methods for adding and removing items from the list.

Here is a simple example of Composite pattern-

// Composite pattern example in TypeScript

// Item interface
interface Item {
    operation1(): void;
    operation2(): void;
}

// First item class
class Item1 implements Item {
    operation1(): void {
        console.log('Item 1: operation 1');
    }

    operation2(): void {
        console.log('Item 1: operation 2');
    }
}

// Second item class
class Item2 implements Item {
    operation1(): void {
        console.log('Item 2: operation 1');
    }

    operation2(): void {
        console.log('Item 2: operation 2');
    }
}

// Composite class
class ItemGroup implements Item {
    private itemList: Item[] = [];

    operation1(): void {
        for (let item of this.itemList) {
            item.operation1();
        }
    }

    operation2(): void {
        for (let item of this.itemList) {
            item.operation2();
        }
    }

    addItem(item: Item): void {
        this.itemList.push(item);
    }

    removeItem(item: Item): void {
        this.itemList.splice(this.itemList.indexOf(item), 1);
    }
}


// Demo        
const item1 = new Item1();
const item2 = new Item2();
const anotherItem1 = new Item1();

const itemGroup = new ItemGroup();
itemGroup.addItem(item1);
itemGroup.addItem(item2);
itemGroup.addItem(anotherItem1);

itemGroup.operation1();
itemGroup.operation2();

Output generated by the code above is –

Item 1: operation 1
Item 2: operation 1
Item 1: operation 1

Item 1: operation 2
Item 2: operation 2
Item 1: operation 2

Examples

Here are a few Composite pattern examples-

Example #1: Transport List

For this example, we want to use a list of transport vehicles and single vehicle the same way.

Transport Interface [Item Interface]

  • Create a file “transport.ts”.
  • Create interface “Transport”.
  • Declare methods “start”, “stop”, “operate” in the interface.
// transport.ts

interface Transport {
    start(): void;
    operate(): void;
    stop(): void;
}

export default Transport;

Bike Class [Item Class]

  • Create a file “bike.ts”.
  • Create class “Bike”.
  • Implement “Transport” interface for the class. Define method – “start”, “stop”, “operate” as part of the interface implementation.
// bike.ts

import Transport from "./transport";

class Bike implements Transport {
    start(): void {
        console.log("Starting Bike...");
    }

    operate(): void {
        console.log("Riding Bike");
    }

    stop(): void {
        console.log("Stopping Bike...");
    }
}

export default Bike;

Plane Class [Item Class]

  • Create a file “plane.ts”.
  • Create class “Plane”.
  • Implement “Transport” interface for “Plane” class.
// plane.ts

import Transport from "./transport";

class Plane implements Transport {
    start(): void {
        console.log("Starting Plane...");
    }

    operate(): void {
        console.log("Flying Plane");
    }

    stop(): void {
        console.log("Stopping Plane...");
    }
}

export default Plane;

Car Class [Item Class]

  • Create a file “car.ts”.
  • Create class “Car”.
  • Implement “Transport” interface for the “Car” class.
// car.ts

import Transport from "./transport";


class Car implements Transport {
    start(): void {
        console.log("Starting Car...");
    }

    operate(): void {
        console.log("Driving Car");
    }

    stop(): void {
        console.log("Stopping Car...");
    }
}

export default Car;

Composite Class

  • Create a file “transport-group.ts”.
  • Create class “TransportGroup”.
  • Define a private property to store a list of transports.
  • Implement “Transport” interface for the “TransportGroup” class. In the method implementation call methods from the individual transport in a loop.
  • Define method for adding and removing transports to the list.
// transport-group.ts

import Transport from "./transport";

class TransportGroup implements Transport {
    private transportList: Transport[] = [];

    start(): void {
        for (let transport of this.transportList) {
            transport.start();
        }
    }

    operate(): void {
        for (let transport of this.transportList) {
            transport.operate();
        }
    }

    stop(): void {
        for (let transport of this.transportList) {
            transport.stop();
        }
    }

    addTransport(transport: Transport): void {
        this.transportList.push(transport);
    }

    removeTransport(transport: Transport): void {
        this.transportList.splice(this.transportList.indexOf(transport), 1);
    }
}

export default TransportGroup;

Demo

To use the composite transport group, use the add method and pass transport object, to add to the list. Then call the methods on the group object.

// demo.ts

import Bike from "./bike";
import Car from "./car";
import Plane from "./plane";
import TransportGroup from "./transport-group";

const bike = new Bike();
const plane = new Plane();
const car = new Car();
const secondCar = new Car();

const transports = new TransportGroup();
transports.addTransport(bike);
transports.addTransport(plane);
transports.addTransport(car);
transports.addTransport(secondCar);

console.log("-----------------Output with 4 transports------------------\n");

transports.start();
transports.operate();
transports.stop();

console.log("\n-----------------Output when plane is removed------------------\n");

transports.removeTransport(plane);

transports.start();
transports.operate();
transports.stop();

Output

The output of the demo above will be like below.

-----------------Output with 4 transports------------------

Starting Bike...
Starting Plane...
Starting Car...
Starting Car...
Riding Bike
Flying Plane
Driving Car
Driving Car
Stopping Bike...
Stopping Plane...
Stopping Car...
Stopping Car...

-----------------Output when plane is removed------------------

Starting Bike...
Starting Car...
Starting Car...
Riding Bike
Driving Car
Driving Car
Stopping Bike...
Stopping Car...
Stopping Car...

Example #2: Player Group

Here we are considering players for different types of sports. We will create a composite class so that we can perform operations on a bunch of players all together.

Player Interface [Item Interface]

  • Create a file “player.ts”.
  • Create interface “Player”.
  • Declare method for the interface. Here we have method – “printDetails”, which is used to print full details of the player.
// player.ts

interface Player {
    printDetails(): void;
}

export default Player;

Basketball Player Class [Item Class]

  • Create a file “basketball-player.ts”.
  • Create interface “BasketballPlayer”.
  • Define private property – “name”, “age”, and “point”. In the constructor, accept the property values and set that.
  • Implement “Player” interface for the class.
// basketball-player.ts

import Player from "./player";

class BasketballPlayer implements Player {
    private name: string;
    private age: number;
    private point: number;

    constructor(name: string, age: number, point: number) {
        this.name = name;
        this.age = age;
        this.point = point;
    }

    printDetails(): void {
        console.log("Game: Basketball");
        console.log("Name: " + this.name);
        console.log("Age: " + this.age);
        console.log("Points: " + this.point);
    }
}

export default BasketballPlayer;

Football Player Class [Item Class]

  • Create a file “football-player.ts”.
  • Create interface “FootballPlayer”.
  • Define private property – “name”, “age”, and “goal”. In the constructor, accept the property values and set that.
  • Implement “Player” interface for the class.
// football-player.ts

import Player from "./player";

class FootballPlayer implements Player {
    private name: string;
    private age: number;
    private goal: number;

    constructor(name: string, age: number, goal: number) {
        this.name = name;
        this.age = age;
        this.goal = goal;
    }

    printDetails() {
        console.log("Game: Football");
        console.log("Name: " + this.name);
        console.log("Age: " + this.age);
        console.log("Goals: " + this.goal);
    }
}

export default FootballPlayer;

Cricket Player Class [Item Class]

  • Create a file “cricket-player.ts”.
  • Create interface “CricketPlayer”.
  • Define private property – “name”, “age”, and “run”. In the constructor, accept and set property values.
  • Implement “Player” interface for the class. Define “printDetails” method as part of interface implementation.
// cricket-player.ts

import Player from "./player";

class CricketPlayer implements Player {
    private name: string;
    private age: number;
    private run: number;

    constructor(name: string, age: number, run: number) {
        this.name = name;
        this.age = age;
        this.run = run;
    }

    printDetails(): void {
        console.log("Game: Cricket");
        console.log("Name: " + this.name);
        console.log("Age: " + this.age);
        console.log("Runs: " + this.run);
    }
}

export default CricketPlayer;

Player Group

  • Create file “player-group.ts”.
  • Create class “PlayerGroup”.
  • Define private property “playerList” which is an array of type “Player”.
  • Implement “Player” interface for “PlayerGroup”. In the method implementation call the methods from individual player object in a loop.
  • Implement “addElement” and “removeElement” for managing the list of players. 
// player-group.ts

import Player from "./player";

class PlayerGroup implements Player {
    private playerList: Player[] = [];

    printDetails(): void {
        for (let player of this.playerList) {
            player.printDetails();
        }
    }

    addElement(transport: Player): void {
        this.playerList.push(transport);
    }

    removeElement(transport: Player): void {
        this.playerList.splice(this.playerList.indexOf(transport), 1);
    }
}

export default PlayerGroup;

Demo

Create a bunch of player objects, and add those to the group. Now we can perform operations on the list of players.

We can also create a group of other players groups. Check the demo/example below.

// demo.ts

import BasketballPlayer from "./basketball-player";
import CricketPlayer from "./cricket-player";
import FootballPlayer from "./football-player";
import PlayerGroup from "./player-group";

// Under 15 players
const under15Players = new PlayerGroup();

under15Players.addElement(new FootballPlayer("FPlayer 15_1", 13, 23));
under15Players.addElement(new FootballPlayer("FPlayer 15_2", 14, 30));

under15Players.addElement(new BasketballPlayer("BPlayer 15_1", 12, 80));
under15Players.addElement(new BasketballPlayer("BPlayer 15_2", 14, 100));

under15Players.addElement(new CricketPlayer("CPlayer 15_1", 14, 467));


// Under 19 Players
const under19Players = new PlayerGroup();

under19Players.addElement(new FootballPlayer("FPlayer 19_1", 18, 43));

under19Players.addElement(new BasketballPlayer("BPlayer 19_1", 17, 77));

under19Players.addElement(new CricketPlayer("CPlayer 19_1", 18, 654));
under19Players.addElement(new CricketPlayer("CPlayer 19_2", 16, 789));


// National team players
const nationalTeamPlayers = new PlayerGroup();
nationalTeamPlayers.addElement(new FootballPlayer("FPlayer N_1", 18, 43));
nationalTeamPlayers.addElement(new BasketballPlayer("BPlayer N_1", 17, 77));
nationalTeamPlayers.addElement(new CricketPlayer("CPlayer N_1", 18, 654));


// Create a group with all teams
const allTeams = new PlayerGroup();
allTeams.addElement(under15Players);
allTeams.addElement(under19Players);
allTeams.addElement(nationalTeamPlayers);

// Print details of all players
// from each game and group
allTeams.printDetails();

Output

We will get the following output-

Game: Football
Name: FPlayer 15_1
Age: 13
Goals: 23

Game: Football
Name: FPlayer 15_2
Age: 14
Goals: 30

Game: Basketball
Name: BPlayer 15_1
Age: 12
Points: 80

Game: Basketball
Name: BPlayer 15_2
Age: 14
Points: 100

Game: Cricket
Name: CPlayer 15_1
Age: 14
Runs: 467

Game: Football
Name: FPlayer 19_1
Age: 18
Goals: 43

Game: Basketball
Name: BPlayer 19_1
Age: 17
Points: 77

Game: Cricket
Name: CPlayer 19_1
Age: 18
Runs: 654

Game: Cricket
Name: CPlayer 19_2
Age: 16
Runs: 789

Game: Football
Name: FPlayer N_1
Age: 18
Goals: 43

Game: Basketball
Name: BPlayer N_1
Age: 17
Points: 77

Game: Cricket
Name: CPlayer N_1
Age: 18
Runs: 654

Source Code

Use the following link to get the source code:

Other Code Implementations

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

Leave a Comment


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