Understanding the dependency inversion principle in TypeScript
Tags: typescript
Published: 2022-03-21 15:06:07

**Written by [Samuel Olusola](https://blog.logrocket.com/author/samuelolusola/)✏️**

Whenever we are building software, developers usually like to break down large problems into smaller ones. We implement the solutions to the subproblems in components or modules, then compose those components together to create a system that solves the original problem.

One of the things that determines the quality of the systems and applications we build is the degree of interdependence between the software modules that make up the project. This degree of interdependence is referred to as a dependency, or coupling.

For example, if we have a module (or class) A that uses another module (or class) B, we say that A depends on B, like so:

```typescript
  class B {
  }
  class A {
      private b = new B();
  }
  // class A depends on class B
```

Below is a diagram representing the relationship between A and B:

![Diagram representing the interdepence between class A and B](https://blog.logrocket.com/wp-content/uploads/2022/03/Diagram-representing-interdepence-class-A-B.png)

But before we can talk about the dependency inversion principle, we have to first discuss the concept of loose versus tight coupling.

## Low (or loose) coupling vs. tight coupling

One of the signs of a well-structured application (or computer system in general) is having very minimal interdependence (low coupling) between the components or modules of the system. A tightly coupled system of components is not desirable.

One of the negatives of a tightly coupled system is that change in one module will have a ripple effect of changes in other modules that depend on the modified module. Another negative is that a module might become difficult to reuse and test, because its dependent modules must be included.

## The dependency inversion principle

The dependency inversion principle helps us to couple software modules loosely. The principle was arrived at after many years of coupling software modules, and it states that:

*   High-level modules should not import anything from low-level modules; they should both depend on abstractions
*   Abstractions should not depend on concrete implementations; concrete implementations should depend on abstractions

### Depending on abstractions

When we abstract, we can say that we are dealing with just the overall idea of something without caring about the details. One important way we abstract things is by using an interface.

The first principle states that both high-level and low-level modules should depend on the same abstractions. If a module depends on an abstraction — say, an interface or abstract class — we can swap the its dependency for any other implementation that adheres to the interface.

For a real-life analogy, consider the plug of a laptop charger below:

![laptop charger plug](https://blog.logrocket.com/wp-content/uploads/2022/03/laptop-charger-plug.png)

This plug can be connected to any socket as long as the socket satisfies the “interface” of the three pins. This way, the plug can be used with a variety of sockets so long as they meet the requirements of the interface.

## An example of dependency inversion in TypeScript

Suppose we have a `Logger` class for logging information in our project, defined in a file `logger.ts`, as follows:

```typescript
export class Logger {
  log(message: string) {
    console.log(message);
  }
  error(message: string) {
    console.error(message);
  }
}
```

We are exporting the `Logger` class because it’s expected to be used elsewhere. We are using it in another module located in a file named `user-service.ts`:

```typescript
import { Logger } from "./logger"; // this module depends on concrete implementatioon of logger

class UserService {
  private logger = new Logger();

  async getAll() {
    try {
      this.logger.log('Retrieving all users...');
      return [];
    } catch (error: any) {
      this.logger.log(`An error occurred: ${error?.message}`);
      throw new Error('Something went wrong')
    }
  }
}
```

Let’s represent the relationship between both classes as follows:

![userservice vs logger relationship](https://blog.logrocket.com/wp-content/uploads/2022/03/userservice-vs-logger-relationship.png)

The `UserService` class depends on the `Logger` class. This is first made obvious from the fact that we have to import the `Logger` class into the `UserService` class.

The line `private logger = new Logger();` also reveals that the `UserService` is tightly coupled to the current implementation of `Logger`. It depends on a concrete implementation.

## Injecting dependencies

`UserService` cannot be tested separately on its own without the `Logger`. One way to improve the testability of `UserService` is to change the way we supply the `Logger` object.

Currently, a `Logger` object is instantiated in `UserService` by the line `private logger = new Logger();`. We can transfer the task of creating the object to another part of our code, and we just use the object directly, i.e., someone else creates the dependency object and injects it into `UserService`. This is called dependency injection.

The code snippet below shows an example of how to do this:

```typescript
import { Logger } from "./logger";

class UserService {
  private logger: Logger;
  // the constructor receives the object to inject from another source
  constructor(logger: Logger) {
    this.logger = logger;
  }
  async getAll() {
    try {
      this.logger.log("Retrieving all users...");
      return [];
    } catch (error: any) {
      this.logger.log(`An error occurred: ${error?.message}`);
      throw new Error("Something went wrong");
    }
  }
}
```

The `Logger` instance is now being injected into the `UserService` class via the constructor, but it still depends on a concrete implementation of `Logger` because the `logger` property is still typed as `Logger`.

## Applying the dependency inversion principle in TypeScript

As suggested by the principle, we want to make sure that `UserService` and `Logger` both depend on abstractions. We don’t want to import anything from the lower level class (`Logger`, in this case) into the high level class (`UserService`).

Let’s introduce an `interface` and let `UserService` depend on it:

```typescript
export interface ILogger {
  log(message: string): void;
  error(message: string): void;
}

class UserService {
  private logger: ILogger;
  constructor(logger: ILogger) {
    this.logger = logger;
  }
  async getAll() {
    try {
      this.logger.log("Retrieving all users...");
      return [];
    } catch (error: any) {
      this.logger.log(`An error occurred: ${error?.message}`);
      throw new Error("Something went wrong");
    }
  }
}
```

Above, we have an `ILogger` interface defined in `UserService`. This service defines what the `UserService` expects of a logger; it’s not tied to a specific logger class.

Any class implementing the interface can be injected into `UserService`. Note that the `ILogger` interface doesn’t have to be defined in the `user-service.ts` file, it can (and should) be extracted into its own file.

Now, we update the `Logger` class to implement the interface defined by `UserService`:

```typescript
import { ILogger } from "./user-service";

export class Logger implements ILogger {
  log(message: string) {
    console.log(message);
  }
  error(message: string) {
    console.error(message);
  }
}
```

Note that `UserService` is not importing anything from `Logger` in its implementation, and both classes depend on an interface (an abstraction).

We can then do this anywhere we want to use `UserService`:

```typescript
...
// inject any class that implements ILogger into UserService
const userService = new UserService(new Logger()) 
userService.getAll()
...
```

The class diagram will look like this now:

![dependency inversion class diagram](https://blog.logrocket.com/wp-content/uploads/2022/03/dependency-inversion-class-diagram.png)

Notice the direction of the arrows. In the previous class diagram, there was an arrow from `UserService` to `Logger` indicating that it depends on `Logger`. Now we have an arrow from `UserService` to the interface `ILogger`. Also, we have an arrow from `Logger` to `ILogger`. Both of them depend on an abstraction.

We have successfully inverted the direction of the dependency. Inversion doesn’t mean that the lower-level layer depends directly on the higher-level level, it means that both layers should depend on abstractions — and these abstractions expose the behavior needed by the higher-level layers.

## Swapping different implementations

Now to show one of the benefits of applying the dependency inversion principle, let’s assume that our project is a Node.js project, and we want to use a third-party logger library for logging. [Winston](https://www.npmjs.com/package/winston) is one of the popular logger libraries we can choose. Because this post is not a post about logging, I won’t be going into the details of how to use Winston.

Say you have installed `winston` and set it up as described in the [docs](https://github.com/winstonjs/winston#readme), you can create a class that implements `ILogger` as follows in a file named `winston-logger.ts`:

```typescript
import { ILogger } from "./user-service";
import * as winston from "winston";

export class WinstonLogger implements ILogger {
  private winstonLogger: winston.Logger;

  constructor() {
    this.winstonLogger = winston.createLogger({
      transports: [
        new winston.transports.File({
          format: winston.format.combine(
            winston.format.json(),
            winston.format.timestamp()
          ),
          filename: "logs/combined.log",
        }),
      ],
    });
  }

  log(message: string): void {
    this.winstonLogger.log(message);
  }
  error(message: string): void {
    this.winstonLogger.error(message);
  }
}
```

We set up the `winston.transports.File` transport so that logs can be saved into a file located at the filepath `logs/combined.log`, relative to the project root. The `WinstonLogger` class also implements the `ILogger` interface.

`UserService` can make use of the `WinstonLogger`, because it adheres to the interface that `UserService` implements as follows:

```typescript
const userService = new UserService(new WinstonLogger())
userService.getAll()
```

The class diagram representing the relationship among the classes looks like this:

![class diagram with WinstonLogger](https://blog.logrocket.com/wp-content/uploads/2022/03/class-diagram-with-WinstonLogger.png)

Suppose that after some time you decide that the `winston` logger library was not the best logger for your project and you want to use [Bunyan](https://www.npmjs.com/package/bunyan), what do you do? You just need to create a `BunyanLogger` class that implements the `ILogger` interface and it is ready to be used by the `UserService`.

Applying the dependency inversion principle also ensures that we are not tied to a specific third-party logging library. We are not in control of the library, but we are in control of how our project code interacts with it.

##Conclusion

In this article, we have gone through what a dependency is and why we want loose coupling between the components or modules making up our TypeScript application. We also looked at the dependency inversion principle, a practical example of how to apply it, and how it enables us to easily swap implementations.

Dependency inversion is one of the popular SOLID principles, which is an acronym for the first five object-oriented design principles by Robert C. Martin. You can learn more about the rest of the SOLID principles [here](https://en.wikipedia.org/wiki/SOLID).

---

## Writing a lot of TypeScript? [Watch the recording](https://blog.logrocket.com/typescript-meetup-recap/?utm_source=social&utm_medium=organic&utm_campaign=21Q3_WB_TS-Tech-Meetup0930#utm_source=social&utm_medium=organic&utm_campaign=21Q3_WB_TS-Tech-Meetup0930) of our recent TypeScript meetup to learn about writing more readable code.

[![Write More Readable Code with TypeScript 4.4](https://blog.logrocket.com/wp-content/uploads/2021/07/typescript-4-4-more-readable-code.png)](https://blog.logrocket.com/typescript-meetup-recap/?utm_source=social&utm_medium=organic&utm_campaign=21Q3_WB_TS-Tech-Meetup0930#utm_source=social&utm_medium=organic&utm_campaign=21Q3_WB_TS-Tech-Meetup0930)

TypeScript brings type safety to JavaScript. There can be a tension between type safety and readable code. Watch the recording for a deep dive on some new features of TypeScript 4.4.