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.
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.
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.
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.
tip 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.
class Class_1 {
method() {}
}
class Class_2 {
class_1_obj
constructor() {
this.class_1_obj = new Class_1()
}
}
let 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.
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
}
}
let 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.
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.
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.
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.