Applying SOLID principles to your TypeScript code

JavaScript TypeScript

The SOLID principles were defined quite some time ago and are still relatable. Their goal is to make our software easier to understand, read, and extend. We accredit this concept to Robert C. Martin from his paper from the year 2000. The actual SOLID acronym was defined later, though. In this article, we go through all of the SOLID principles and reflect them in TypeScript examples.

Single responsibility principle

The single responsibility principle declares that a class should only be responsible for a single functionality. The above also refers to modules and functions.

We should split the above class into two separate ones. SOLID defines the concept of responsibility as a reason to change.

Robert C. Martin, in his blogpost, discusses how we can define a reason to change. He stresses that it shouldn’t be deliberated in terms of code. For example, refactoring is not a reason to change in terms of the single responsibility principle.

To define a reason to change, we need to investigate what is the responsibility of our program.

The Statistics class might be changed due to two different reasons:

  • the logic of the sales statistic computation changes,
  • the format of the report changes

The single responsibility principle highlights that the two above aspects put two different responsibilities on the Statistics class.

Now we have to separate classes, and each of them has a single reason to change, and therefore just one responsibility. Applying to the above principle makes our code easier to explain and understand.

Open-closed principle

According to the open-closed principle, software entities should be open for extension but closed for modification.

The core idea of the above principle is that we should be able to add new functionalities without changing the existing code.

Let’s say we want to create a function that calculates the area of an array of shapes. With our current design, it might look like that:

TypeScript understand what properties our shapes have thanks to control-flow based narrowing.
We discuss that subject a bit in Understanding any and unknown in TypeScript. Difference between never and void

The issue with the above approach is that when we introduce a new shape, we need to modify our   function. This makes it open for modification and breaks the open-closed principle.

We can fix that by enforcing our shapes to have a method that returns the area.

Now that we are sure that all of our shapes have the   function, we can use it further.

Now when we introduce a new shape, we don’t need to modify our   function. We make it open for extension but closed for modification.

We could achieve the above functionality also by using an abstract class instead of an interface

Liskov substitution principle

The above rule, introduced by Barbara Liskov, also helps us ensure that changing one area of our system does not break other parts. To make this principle less confusing, we will break it down into multiple parts.

Replacing an instance of a class with its child class should not produce any negative side effects

The first thing we notice in the above principle is that its main focus is class inheritance.

Let’s implement a straightforward and vivid example of how we can break the above principle.

The above code is very problematic because the implementation of   differs in the   and the  .

The above code works fine, but when we replace an instance of a parent class with its child class, we experience issues.

TypeError: this.permissions.has is not a function

This situation is very vivid and shouldn’t happen in a properly typed TypeScript code. We had to use   to allow the   to extend the   improperly.

Validation conditions

A more refined example is with preconditions and validation.

The above example, on the other hand, is fine in terms of typings. Unfortunately, it also breaks the Liskov substitution principle.

Error: Cashier should not be able to delete products!

The same thing applies to output conditions. If the function that we override returns a value, the child class shouldn’t put additional validation on the output.

Interface segregation principle

The interface segregation principle puts an emphasis on creating smaller and more specific interfaces. Let’s imagine the following situation.

Above, we have an interface of a bird. We assume that birds can walk and fly. It is straightforward to create an example of such a bird:

The above is not always the case, though. The above assumption can be incorrect.

The interface segregation principle states that no client should be forced to depend on methods it does not use. By putting too many properties in our interfaces, we risk breaking the above rule.

What we might do instead is to implement smaller interfaces, sometimes referred to as role interfaces.

By changing our approach to interfaces, we avoid bloating them and make our software easier to maintain.

Dependency inversion principle

The core of the dependency inversion principle is that high-level modules should not depend on the low-level modules. Instead, both of them should depend on abstractions.

Let’s create an example to investigate the above further.

The above behavior is elementary and academic, but it is not always the case. If the introduction was more complicated, we might want to create separate classes just for that.

Unfortunately, the above code breaks the dependency inversion principle. It says that we should invert the dependencies of the   and the  .

The benefit of the above is that we don’t need subclasses for the  and the .

The above approach is an example of using composition instead of inheritance.

Also, this makes our classes easier to unit-test, because we can effortlessly provide a mocked service in a constructor.

Summary

In this article, we’ve gone through all of the SOLID principles:

  • S: Single responsibility principle
  • O: Open-closed principle
  • L: Liskov substitution principle
  • I: Interface segregation principle
  • D: Dependency inversion principle

By applying them to our code, we can make it more readable and maintainable. Also, those are good practices that make our codebase easier to expand while not affecting other parts of our application.

Even though SOLID was defined quite some time ago, it shows that it is still relatable, and we can gain some real benefits from understanding them.

Subscribe
Notify of
guest
3 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Jisack
Jisack
3 years ago

Easy to understand with those example.

NgSpeedster
NgSpeedster
2 years ago

Good article.
I would say that your explanation of the Open/Closed principle is more likely to Single Responsibilities, in short: the point is not only to create a separate class but also to make this class easy to change, without affecting parent classes.

Zozo
Zozo
2 years ago
Reply to  NgSpeedster

I would say that ShippingType should be an interface with getCoast method
And then we can create classes “AirShipping”, “GroundShipping”, “SelfShipping” with own logic for each class. In this case open-closed principle will be followed I gues 🙂