Simplifying Dependency Injection and IoC Concepts using TypeScript

Saad Bin Amjad
Monstar Lab Bangladesh Engineering
10 min readOct 2, 2019

It is never easy to do everything by yourself. Since the beginning of time, humans truly understood, often with a huge cost, that their true power lies not in conflicts but in collaboration. Programming paradigm is also not quite different. For an application to live long, it must figure out the dependencies and try to look for ways to delegate others to serve the dependencies it needs. Not only this can solve its own problem efficiently, but it also, in turn, help other programs by presenting itself as their dependency. Dependency Injection is a crucial application design pattern for almost all the frameworks out there to create reusable, manageable and testable code. Today let’s try to simplify Dependency Injection, which is a subset of Inversion of Control principle, with TypeScript.

But first, let us set the table straight. If you have noticed any top class restaurant’s kitchen operation then you will find this article’s motivation a bit similar. In a restaurant, each Station Chef is responsible for running a specific section of the kitchen and they are being managed directly by the Head Chef or by the second-in-command Sous Chef. Station chefs can be in charge of different things respectively. A list of station chefs includes but is not limited to butcher chef, fish chef, grill chef, pantry chef, etc.

Whenever one order comes to the kitchen via a Caller, the Sous Chef simply doesn’t start preparing it all by himself. He runs down what are the things he needs to deliver, and start instructing them accordingly to each Station Chef. He expects they would handover their prepared items on a common table, where the entire dish can then be prepared or garnished for serving. This is an important takeaway that we will soon find out in this article but before that here is a rundown of the topics you will get introduced in this article.

- Dependency Injection
-
Dependency Inversion Principle
-
Inversion of Control
- Inversion of Control Container
- TypeScript Interfaces
- Decorator Functions
- Reflection APIs
- TypeScript IoC

Problem Statement

Let’s present a problem we are going to recreate and solve, called “Pizza making chronicles”.

I made this comic strip using Pixton. It is a cool app, do check out!

Pizza making got several dependencies to start with. Ignoring cheese and toppings (even sauce) for simplicity, we can see that pizza needs dough and dough needs flour and yeast. Flour and Yeast need water. Water needs salt. Here is a directed graph diagram that roughly shows these dependencies:

The blue line shows the dependencies of dough.

Dependency Injection

Dependency injection is a fancy phrase that essentially means this: class dependencies are “injected” into the class via the constructor or, in some cases, “setter” methods.

Pizza needs dough to start with. Let’s see how does that look in a typical code base.

Soon we figure out that this Dough class is also required for making bread, so its redundant to allow both Pizza and Bread to be in charge of Dough all by themselves. So let’s delegate the creation of Dough to someone else. We inject the dependency Dough in Pizza constructor, an example of the most famous constructor based Dependency Injection.

However, we passed a concrete implementation of Dough in Pizza, called DoughEntity. But customers might order different types of dough for different pizzas later.

Dependency Inversion Principle

..DIP says that our classes should depend upon abstractions, not on concrete details.” — Robert C. Martin

Since dependency implementations can be swapped easily as they are injected during runtime, we rather inject an interface, like IDough so that we can later swap it with any concrete implementation of IDough, i.e DoughEntity like SourdoughDough, BriocheDough, ChallahDough, FocacciaDough etc.

You can read more about the benefits of having interface defining an action and other classes implementing the interface in another article of mine:

With dependency injections, we ensured that the chef making pizza can’t make its own dough, and with dependency inversion principle we ensured that different kinds of dough can now be used for making pizza. Same goes for dough maker. Dough maker doesn’t decide which flour and what kind of flour it will use rather it is handed to him by someone else in the charge of the pantry.

There is a slight problem with this pattern in applications. This might lead to a nested and complicated dependency graph because we keep manually initiating the dependencies and pass them up to the ones who need it. So what to do?

Inversion of Control

In software engineering, inversion of control (IoC) is a programming principle. IoC inverts the flow of control as compared to traditional control flow. In IoC, custom-written portions of a computer program receive the flow of control from a generic framework.

Dependency Injection so far looked like this: The Caller needs a pizza and announces it in the kitchen. Pizza Chef doesn’t make the dough himself, he asks it from the dough maker.

But imagine the dough maker now says, “Don’t ask me for the dough, I will keep my dough on a table when I am done myself.”

There is a Hollywood Principle, which states:

“Don’t Call Us, We’ll Call You”

This is what inversion of control looks like. The Pizza Chef doesn’t directly gets the dough from the dough maker, rather he will get if from an external place without him having a knowledge of who or when it was exactly kept there.

Inversion of Control Container

The basic idea is that on startup of your application, you can define mappings between your abstractions and your implementations — eg. between your interfaces and concrete types. Then the IoC Container library will handle creating your objects for you and will automatically inject any dependencies.

So finally we have a kitchen table which contains the prepared items. Pizza chef takes everything from there when he needs it and starts making pizza. A programming container also behaves as such, which typically bind interfaces to implementations and serves them as dependencies when someone needs it. It will look something like this in the code.

Now let’s code the talk using TypeScript. The demo code is a NodeJS application build using Express framework. We will be using InversifyJS, an IoC container for TypeScript, in this project. Here are the dependencies used:

I used lightweight InversifyJS to simply demonstrate how containers work in applications, and then it will be easier for us to understand how frameworks like Angular or Laravel uses DI and IoCs. You can check out the entire repository here:

TypeScript Interfaces

Let us create an interface called Dough. I don’t like using IDough naming, so would name dough as Dough for the rest of the code.

TypeScript — interfaces are only used when you’re writing code (the editor can show you errors) and when you compile. They’re not used at all in the generated JavaScript.

We usually want to code using interfaces so that we have better consistency across classes, but in the generated JavaScript, we do not have any references for it. It is simply non-existent. So how can we use Dependency Injection in TypeScript?

Decorator Functions

A Decorator is a special kind of declaration that can be attached to a class declaration, method, accessor, property, or parameter. Decorators use the form @expression, where expression must evaluate to a function that will be called at runtime with information about the decorated declaration.

Frameworks like Angular and NestJS use decorators, so to tell its own DI mechanism what are the dependencies other classes require and and how to initialise them.

These decorator functions basically add metadata to our classes. The metadata are used to gain information about what dependencies it needs during runtime and thus helps in resolving them.

In Inversify, whenever a class (it will always have @injectable decorators on them) have a dependency on an interface, we use the @inject decorator to define an identifier for the interface that will be available at runtime.

Reflection APIs

Reflection allows inspection and modification of a code base, including its own. Unlike JavaScript, TypeScript does support experimental reflection features, though few in number. Using metadata reflection API we can standardize how we achieve details of unknown objects during runtime.

For the class Pizza, when DoughEntity dependency is called, the Injectable Decorator adds a metadata entry for the property using the Reflect.metadata function from the reflect-metadata library, which gives us a array of dependencies that the class requires. It under the hood presents us with the right information about DoughEntity being a dependency for Pizza.

Here is a simplified example of the metadata generated when we pass a concrete implementation. You can find this behaviour in NodeJS frameworks like NestJS, and its decorator usage .

We can see that the metadata points to exact DoughEntity. But what if we injected Dough interface?

When the code transpiles to JavaScript, we get metadata as an Object, with no way for us to say which specific object it is. We need to do something to uniquely identify them so that during runtime the proper class is resolved.

In our demo code after having coded in the interfaces, we type-hint our real classes instead of interfaces. We use Symbol to allow identification of Dough interface with “Dough”.

Let’s create a concrete implementation of Dough now and call it DoughEntity. Here you can see, we mentioned the class as injectable using @injectable decorator, and passed in interfaces in the constructor with @inject decorator so to define the identifiers.

Now we create a container that binds the interfaces to their implementations such that if Dough types are called, we get concrete implementation of DoughEntity.

We create the Pizza class as following:

Before we do the rest of the code, here is a peek in to the future for better understanding. Since we are using decorators and reflection API, if we see the generated JavaScript code of pizza.class.ts by setting “emitDecoratorMetadata”: true in tsconfig.json, we could see that the Object we get in metadata will be of type Types.Dough.

And the generated JavaScript code of dough.entity.ts would look like this, where param orders tell us about DoughEntity dependencies:

It surely seems that the decorators and reflection API have done its part in figuring out the dependency.

And now the moment of truth, let’s resolve a dependency here in the main.ts file and see it in action.

If we run the application and open the console, we can see the following output telling us the Dependency Injection worked using the IoC container.

Salt gets initiated first. Water, Flour, Yeast and finally Dough classes were successfully resolved and finally got injected into the Pizza class.

Conclusion:

Photo by David Cain on Unsplash

There is a reason why good kitchens operate similar to this. A real chef can vouch about it more than me for sure. They can truly deliver the best when each of the chefs has singular, separate responsibilities, each knowing exactly what, where and when to contribute in making a perfect dish for the restaurant-goers, with someone managing it, who is however not directly being involved in any of the process details.

I am really impressed with the DIs and IoCs of Laravel, Angular and NestJS. It has really made both the backend and frontend code much more manageable, reusable and testable with time. The key concepts are the same in all the frameworks and are rightly so. Feel free to reach out to me on Twitter (@saadbinamjad), and you can check some other articles posted by me in our Engineering Blog. If you want to read more on some diversified topics, please check out our company blog.

Thanks, till then happy coding!

--

--

Saad Bin Amjad
Monstar Lab Bangladesh Engineering

Technical Lead at Monstar Lab Bangladesh. Prefers tea over coffee, and coffee over anything else. Loves to speak.