TypeScript Composition Tutorial

In this TypeScript tutorial we learn how to design loosely coupled applications with composition and why you should favor it above inheritance.

We cover how to instantiate a class within another class and how to access class members through multiple objects.

Lastly, we take a look at the pro's and cons of inheritance and composition.

Here's a table of contents of what you'll learn in this lesson:
(click on a link to skip to its section)

Let's jump right in.

What is composition

Composition is a type of relationship between classes where one class contains another instead of inheriting from another.

Composition should be favored above inheritance because it’s more flexible and allows us to design loosely coupled applications.

As an example, let’s consider an application that converts a video from one format to another. Typically, we would create a converter class that contains the details of the file and the functionality to convert it.

Then, we could have another class that inherits from the converter and sends a notification to the user when it’s done, pulling details like the name, size etc. from the parent converter class.

But what if we wanted to add audio file conversion to our application. We would have to write another notification class, this time inheriting from the audio converter.

This creates two problems:

  • The first problem is that we have two child classes that essentially do the same thing.
  • The second problem is that the children heavily depend on their respective parents. If we needed to change something in the parent converter classes, we could break the functionality of the children.

The solution is composition. Where inheritance can be thought of as an is-a relationship, composition can be thought of as a has-a relationship.

Instead of creating a child class that inherits functionality from a parent class, we create stand-alone classes that contain other classes.

Simply put, we create an object instance of a class inside another class.

Convert inheritance to composition

We can convert any inheritance relationship to composition. To create this association between classes, we instantiate a new class object inside the constructor of the class we want to use it.

Syntax:
class Class_1 {}

class Class_2 {

  object_name;

  constructor() {
    // create object of Class_1
    this.object_name = new Class_1();
  }

}

It’s similar to how we construct properties, except this time we create a new instance object of another class.

Example:
class Pushable {

  sendPushMessage() {
    console.log("Your file has been converted successfully.");
  }
}

class Converter {

  push;

  constructor() {
    this.push = new Pushable();
  }

  convert() {
    console.log("Converting...");
    return true;
  }
}

The Converter class now has access to the functionality to sendPushMessage() without inheriting from the ‘Pushable’ class.

Functionality classes like Pushable are often named with “able”, “can” or “has”. Like Flyable, CanDrive, HasFood etc.

How to access an object in a class

To access an object in a class we go another level deeper with dot notation. Let’s see the syntax first, then break it down.

Syntax:
class Class_1 {

  method() {}
}

class Class_2 {

  class_1_obj;

  constructor() {
    this.class_1_obj = new Class_1();
  }

}

var class_2_obj = new Class_2();

class_2_obj.class_1_obj.method();

Before we can access anything in ‘Class_2’, we have to create an object of it. Once we have the class_2_obj, we can access the class_1_obj with dot notation.

Because class_1_obj is an object itself, we have to use another level of dot notation to access its method. An example will illustrate it better.

Example:
class Pushable {

  sendPushMessage() {
    console.log("Your file has been converted successfully.");
  }
}

class Converter {

  push;

  constructor() {
    this.push = new Pushable();
  }

  convert() {
    console.log("Converting...");
    return true;
  }
}

var c1 = new Converter();

c1.convert();
c1.push.sendPushMessage();

In the example above we access the push object that was created in the ‘Converter’ class constructor, from the ‘c1’ object. From there, we simply access the sendPushMessage() through dot notation.

Why favor composition over inheritance

You might be asking, if composition only gives us indirect access, why should we use it?

Well, inheritance can be abused very easily. It could lead to a large hierarchy of classes that depend on each other, which is fragile.

If we change a class at the top of the hierarchy, any class that depend on it could be affected and may need to be changed as well.

As an example, let’s consider a class called ‘Animal’ and two child classes that inherit from it.

Example:
class Animal {

  eat() {
    console.log("Eating...");
  }

  walk() {
    console.log("Walking...");
  }
}

class Dog extends Animal {}
class Cat extends Animal {}

The ‘Dog’ and the ‘Cat’ can both eat() and walk(). But if we wanted to add a ‘Fish’, the hierarchy would need to be changed.

We would need something like a ‘Mammal’ class that inherits from ‘Animal’. Then, ‘Dog’ and ‘Cat’ can inherit from ‘Mammal’.

‘Fish’ will have to inherit from ‘Animal’, but also be separate from ‘Mammal’ and depending on what we want to do, ‘Animal’ may have to change.

Now let’s consider that instead of is-a animal, the classes now has-a animal.

Example:
class Animal {

  eat() {
    console.log("Eating...");
  }
}

class Walkable {

  walk() {
    console.log("Walking...");
  }
}

class Swimmable {

  swim() {
    console.log("Swimming...");
  }
}

class Dog {

  animal;
  walkable;
  swimmable;

  constructor() {
    this.animal = new Animal();
    this.walkable = new Walkable();
    this.swimmable = new Swimmable();
  }
}

class Fish {

  animal;
  swimmable;

  constructor() {
    this.animal = new Animal();
    this.swimmable = new Swimmable();
  }
}

This time, we add a class for any animal that can swim, and any animal that can walk.

The ‘Cat’ and ‘Dog’ classes can implement both ‘Walkable’ and’ Swimmable’. The ‘Fish’ class can implement Swimmable, skipping the others.

If we wanted to add a bird, we could add a ‘Flyable’ class, and the bird could implement ‘Walkable’, ‘Swimmable’ and ‘Flyable’.

The application is now loosely coupled, as each class stands on its own and is not dependent on another class. Let’s look at a full example.

Example:
class Walkable {

  walk() {
    console.log("Walking...");
  }
}

class Swimmable {

  swim() {
    console.log("Swimming...");
  }
}

class Flyable {

  fly() {
    console.log("Flying...");
  }
}

class Fish {

  swimmable;

  constructor() {
    this.swimmable = new Swimmable();
  }
}

class Bird {

  walkable;
  swimmable;
  flyable;

  constructor() {
    this.walkable = new Walkable();
    this.swimmable = new Swimmable();
    this.flyable = new Flyable();
  }
}

console.log("Nemo the fish's activities:");
var nemo = new Fish();
nemo.swimmable.swim();

console.log("Tweety the bird's activities:");
var tweety = new Bird();
tweety.walkable.walk();
tweety.swimmable.swim();
tweety.flyable.fly();

Inheritance vs Composition: Pro’s and Cons

We don’t mean that inheritance is a bad thing, a developer will still need to use inheritance from time to time.

Composition is just an alternative that we should consider, before using inheritance.

Let’s look some pro’s and cons of both inheritance and composition.

  • Inheritance Pro’s: Reusable code, easy to understand
  • Inheritance Cons: Tightly coupled, fragile, can be abused

  • Composition Pro’s: Reusable code, flexibility, loosely coupled

  • Composition Cons: Harder to understand

Summary: Points to remember

  • Composition is instantiating and accessing a class inside another instead of inheriting from it.
  • Any inheritance relationship can be converted into composition.
  • A class that’s instatiated inside another must have the this keyword to refer to the calling object.
  • Access to the inner object is done via dot notation, multiple levels deep.
  • We should typically try to favor composition over inheritance.