Simplifying Dependency Injection and IoC Concepts using TypeScript
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”.
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:
Dependency Injection
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
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.
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
, whereexpression
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:
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!