Vigneshwaran
4 min readApr 15, 2021

--

  • Typescript: Dependency Injection & Dependency Inversion made easy

This will be a comprehensive yet beginner-friendly article revolving around dependency injection and inversion of control in a layered architecture using inversifyJS.

Before I begin, this is the directory structure that I will be dealing with in this article.

Directory structure of project

Firstly, what do you mean by dependency injection?

Let's consider a CRUD API(refer to below code), the routes receive the request at different endpoints and calls the appropriate controller functions. As per the request, subsequent Repo functions are called from the controller. The Repo handles DTOs and DAOs.

App.ts

Routes.ts

Controller.ts

Repo.ts

Any beginner programmer would find this as a perfectly written code that compiles at the very first try(without errors ofc!). But here is the catch, you see when you call routes() from app.ts you need an instance of class Routes. When you go into routes, you need an instance of class Controller and in class Controller, you need an instance of class Repo.

You will be needing something like,

new Routes(new Controller(new Repo).

Still, looks pretty safe. This shows that your code is now highly coupled to one another(which is a bad practice). But the problem is when you want to unit test individually. You cannot ensure a proper flow without the instances of every class involved. The testability of your code is now a headache!. This will be frustrating when your codebase is huge.

TL;DR: avoid using new() in constructors

*Enter* Dependency Injection

Dependency injection is a technique whereby one object supplies the dependencies of another object.

Instead of creating an instance from inside a constructor, pass an instance at runtime to the constructor.

This would ensure the testability of the Routes class which is dependent on the controller instance. You just to pass an object when you unit-test this class.

Takeaway

  • Avoid creating instances in the constructor (avoid using new())
  • Make it a practice to write loosely coupled code.

DI using Inversion of Control(IoC) principle

IoC principle ensures that the class in itself is not responsible for the creation or supply of dependencies.

These are broader terms used hand-in-hand at times. For beginners, this may sound weird to wrap their heads around this. IoC is not dependency inversion. In fact, IoC has nothing to with dependencies.

To have a better understanding, go through this StackOverflow thread for further reading.

There are many IoC modules out there to try. Here I will be using inversifyJS.

npm i -g inversify reflect-metadata

InversifyJS uses @injectable() and @inject() decorators for injecting dependencies at runtime. Let’s set up a container that provides objects at runtime.

InversifyJS provides a bind method that binds classes to provide instances at runtime. To obtain the instances, there is a resolve() and get()method provided. In app.ts the instance for routes is obtained as

const route = container.resolve<Routes>(Routes)

In turn, the classes will have injectable and inject decorators to enable the container to inject dependencies.

Pssst! P.S

There are certain prerequisites for inversifyJS.

  • Inversify requires a module reflect-metadata , to use decorators
  • Change the following compiler options in tsconfig.json,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
  • This step is important, import the reflect-metadata module only once in the project(Multiple import objects will cause issues at runtime). Import the module at the start before importing inversifyJS module.

Now, what about dependency inversion? How is that different from dependency injection?

Dependency Inversion

Dependency Inversion is the D in SOLID principles.

The principle states, High-level modules should not depend on low-level modules. Both should depend on abstractions and not concrete implementations (e.g., interfaces).

Tenet Inversion

In our example, routes classdepends on a concrete implementation of controller class. Similarly controller class depends on repo class. Instead of relying directly on implementation, it should be relying on abstraction.
Why is that? Because the primary reason for inversion is the ability to reuse available components.

Let’s say we want to test our Routes class. It will not be easier to write tests if the Routes directly depends on an implementation of Controller. Here is where Inversion comes in handy. An interface ControllerInterface is introduced and our controller class implements this interface, and the routes class depends on this interface. Thus, an abstraction is introduced between both the classes.

Now, it will be easier to test this, because we can simply write a mockController to test Routes class and the mockController implements ControllerInterface.

The above figure shows the introduction of interfaces. If we want to add functionalities in the future we can simply make a new implementation of the interface rather than creating a new class altogether.

We need to makes changes in our container to bind the interface to appropriate classes(During injection we mapped it to the class, not interfaces).

I hope this article gave you a proper introduction to both dependency injection and inversion. The complete code for this tutorial is available on GitHub. Please share your thoughts on this guide in the comments.

Cheers!!

--

--

Vigneshwaran

A passionate nodejs developer by the day, and a bug bounty hunter by the night.