Dependency Injection in TypeScript from scratch

By Liron Hazan, Senior Frontend Engineer at SentinelOne

Image for post
Image for post

In the following post, we’ll review the Dependency Injection as a technique which meant to help us produce a more loosely coupled code which is highly important when working on large-scale projects.

Loosely coupled code? Yep, In a multi-paradigm language, a practical system may contain Classes and Interfaces, putting Object Oriented approach and inheritance aside, a class that points to another concrete class becomes coupled to it and thus harder to refactor/replace implementations.

DI most simple form (that you probably know) is in passing the dependency into the consumer constructor as follows:

Separation of concerns

In the above example, Bar has one concern (one responsibility) and so has Foo.

By injecting Bar into Foo we keep that separation and by doing that we gain:

  1. If in the future we’ll want to use a different strategy, we’ll only need to replace the dependency without changing Foo — cause we managed the dependency outside of it.
  2. In case Bar is a shared service that we aim to instantiate once, we don’t have to implement it as a singleton, that will become the responsibility of the “injecting” layer which IMO keeps things looser.
  3. Unit testing is easier, we can just inject mock implementations into the class which is under test.

When do we need a DI system?

Usually when we bootstrap our program (configuring stuff which depends on one another).

Or when constructing an object which depends on several dependencies. Letting a dedicated third side handle it for us gets us a looser.

In the Javascript/Typescript realm, when working with frameworks such as Angular and Nest.js DI comes out of the box.

And there are libraries such as Inversify which can be used for adding a robust DI system into our product.

How to do it ourselves!

In order to implement our DI system in a generic way we will need to know:

  • How a class in our software is constructed?
  • Which of that class dependencies are actually instances of other classes?
  • How to instantiate in the right order (meaning if A depends on B which depends on C we’ll need to instantiate C first then B and then A)?

You’re probably asking yourself — But what if C depends on A? we’ll that’s circular dependency and that won’t work :)

Reflection to the rescue:

“The ability of a programming language to be its own metalanguage” (Wikipedia)

Meaning that at runtime we can get the metadata of our code parts and act accordingly (observe and modify if needed).

In our case: we’ll need to reflect if a dependency of a class is of type of another class.

But in Typescript we use interfaces, and interfaces only exist in design time, before transpiling the code to Javascript by the Typescript compiler.

What we do have in the Javascript standards today is the Reflect API which contains several static methods for interceptable JavaScript operations. Intercept means — to observe an action before it starts and act accordingly.

In Typescript the Reflect namespace has more options at design time.

TypeScript includes experimental support for emitting certain types of metadata for declarations that have decorators.

** Note: If you’re not familiar with decorators read here.

In order to have that reflective types metadata we’ll need to do the following:

1. Install: ‘reflect-metadata’. (currently, that’s the official recommendation, it will augment the Reflect namespace with the metatypes decorators)

2. In tsconfig enable following “Experimental Options”: “experimentalDecorators”: true, “emitDecoratorMetadata”: true

3. Decorate our injectables (services that other class will consume) so their parameter types metadata be emitted.

Let’s write some code:

Image for post
Image for post

First, we need to have a decorator for collecting metadata of the service class.

When we enabled the compilerOption we instructed the Typescript compiler to add type metadata to any decorated classes.

We’ll create the “Injectable” decorator as follows:

Our decorator is just a function that returns a function that accepts the decorated class as an argument and returns nothing.

Second — decorate the injected service:

Third — creating the injector class — where the work happens

Going over the code we see that the instance resolve(…) method maps a class name to its instance.

The resolve gets the class we want to inject (constructor) and extract the dependencies (tokens) of it by using: Reflect.getMetadata(‘design:paramtypes’, target).

The “design:paramtypes” input is used in order to know which type are the function params (as mention earlier in this post).

To see it all wired together checkout the playable example.

Summing it up, dependency injection IMO is a technique worth knowing, it has many advantages as we saw and keeps our code cleaner and looser, but not every system needs a DI framework, a small system (or library) designed loosely will probably won't need it.



(Liron’s team is hiring- and you are welcome to join her. See open positions here).

This is the tech blog of Sentinelone, a leading cybersecurity company. Follow us to learn about the awesome tech we build here.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store