Composite

Working with tree-like components in a uniform way regardless of whether it's a leaf or a container

This pattern is used for working with nested-container type of items. It's a uniform way to treat an object whether it's another container or actually an item that can do some action.
Here's the recipe of all that's needed to implement the pattern:
  1. General class called "Component" that has basic properties and methods
  2. Class "Leaf" that accepts an identifier as a parameter (a name in the example case). This class can perform a basic operation that is required (just return its name in our example);
  3. Class "Composite" that also takes an identifier as a parameter. It has a few other notable characteristics:
    • Tracks a collection of its children items (leafs or composites) rooted under this composite
    • Processes additions/removals to this collection
    • Primary operation returns its name, as well as triggers a primary operation on all tracked children
The first thing that needs to be done is to extract same functionality between branches and leaves into a class Component. It defines a parameter for a name, and a method that will work in a Leaf and a Brach class.
    class Component { //base class extracting repeating functionality
  name: string;
  primaryOperation(depth: number): void {}

  constructor(name: string) {
    this.name = name;
  }
}
The next piece of code defines Leaf and Branch classes. Branch class can have many leaves (hence the container for leaves called "components" and is of type Component[]). Also, it is worth noting the different implementation of the "primaryOperation" method between the two classes. In the Leaf class, it returns the identifier for the object it is in. For the Branch class, it also returns an identifier, but it also calls the same method for all items in its' components container. This is how the pattern is working, method is the same among both types of objects, but it acts differently between the two types of objects.
    class Leaf extends Component {
  name: string;
  constructor(name: string) {
    super(name);
    this.name = name;
  }
  primaryOperation = (depth: number): void =>
    console.log(Array(depth).join("-") + this.name);
}

class Branch extends Component {
  name: string;
  components: Component[];
  constructor(name: string) {
    super(name);
    this.name = name;
    this.components = [];
  }
  primaryOperation(depth: number): void {
    console.log(Array(depth).join("-") + this.name);
    this.components.forEach((x) => x.primaryOperation(depth + 2));
  }

  add = (component: Component): number => this.components.push(component);

  remove = (component: Component): Component[] =>
    (this.components = this.components.filter(
      (x) => x.name !== component.name
    ));
}
Here's some test code and output:
const root = new Branch("root");
root.add(new Leaf("Leaf 1"));
root.add(new Leaf("Leaf 2"));

const comp1 = new Branch("Subtree 1");
comp1.add(new Leaf("Subtree 1 Leaf 1"));
comp1.add(new Leaf("Subtree 1 Leaf 2"));

const comp2 = new Branch("Sub-Subtree 1");
comp2.add(new Leaf("Sub-Subtree 1 Leaf 1"));
comp1.add(comp2);

root.add(comp1);
root.add(new Leaf("Leaf 3"));

root.primaryOperation(1);

/*   output:
root
--Leaf 1
--Leaf 2
--Subtree 1
----Subtree 1 Leaf 1
----Subtree 1 Leaf 2
----Sub-Subtree 1
------Sub-Subtree 1 Leaf 1
--Leaf 3
*/

Bonus: Composite with Builder

If you notice in the code above that creates a tree, there's some repetition that can be automated using the Builder pattern. In short, our new ConponentTreeBuilder class will have methods to add leafs and composites, as well as let us navigate between composites on the tree to add/remove elements.
class ComponentTreeBuilder {
  rootComponent: Branch;
  currentComponent: Branch;
  constructor(rootComponentName: string) {
    this.rootComponent = new Branch(rootComponentName);
    this.currentComponent = this.rootComponent;
  }

  addComponentItem(name: string): Branch {
    let comp = new Branch(name);
    this.currentComponent.add(comp);
    this.currentComponent = comp;
    return comp;
  }

  addLeaf(name: string): Leaf {
    let leaf = new Leaf(name);
    this.currentComponent.add(leaf);
    return leaf;
  }

  setCurrentComponent(branchName: string): Branch {
    let stack = [];
    stack.push(this.rootComponent); //start from the top
    while (stack.length > 0) {
      let current = stack.pop();
      if (current.name === branchName) { //is that the branch we need?
        this.currentComponent = current; //set it to current component
        return current; 
      }
      let branchesOfCurrent = current.components.filter(
        (x) => typeof x.add === "function" //get only branches, no leafs
      );
      stack.push(...branchesOfCurrent); //work on the next level of branches
    }
    throw new Error(
      `Component name "${branchName}" does not exist in the current hierarchy`
    );
  }
}
    
Here's the code that calls the ComponentTreeBuilder to put together a tree. It seems a bit less verbose than the previous way and it provides for a way to navigate branches easier
    const builder = new ComponentTreeBuilder("root");
    builder.addComponentItem("left branch");
    builder.addLeaf("left branch, leaf 1");
    builder.addLeaf("left branch, leaf 2");
    builder.setCurrentComponent("root");
    builder.addComponentItem("center branch");
    builder.addLeaf("center branch, leaf 1");
    builder.addLeaf("center branch, leaf 2");
    builder.setCurrentComponent("root");
    builder.addComponentItem("right branch");
    builder.addLeaf("right branch, leaf 1");
    builder.addLeaf("right branch, leaf 2");
    builder.setCurrentComponent("center branch");
    builder.addComponentItem("sub-center branch");
    builder.addLeaf("sub-center branch, leaf 1");
    builder.rootComponent.primaryOperation(1);


    /* output:
    root
    --left branch
    ----left branch, leaf 1
    ----left branch, leaf 2
    --center branch
    ----center branch, leaf 1
    ----center branch, leaf 2
    ----sub-center branch
    ------sub-center branch, leaf 1
    --right branch
    ----right branch, leaf 1
    ----right branch, leaf 2
    */