Dependency Injection in TypeScript from scratch
By: Sentinelone Tech
Tags: Coding, Frontend, Computer Science, Cybersecurity, Code

By Liron Hazan, Senior Frontend Engineer at SentinelOne

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:

//Good:

class Bar implements BarStrategyImpl {

doSomething(){}

}

class Foo {

constructor(private bar: BarStrategyImpl){};

doSomething(){

this.bar.doSomething();

}

}// Example without DI:

class Foo {

bar: BarStrategyImpl;

constructor() {

this.bar = new Bar(); // Foo is coupled to Bar

};

doSomething(){

this.bar.doSomething();

}

}

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:

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. 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. 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:

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:

import {Ctr} from '../../../common/types';

type ClazzDecorator<T> = (target: T) => void;



export function Injectable <T>(): ClazzDecorator<Ctr<T>> {

return (target: Ctr<any>) => {

// this is needed so the design:paramtypes could be collected

console.log('inside: Injectable decorator');

console.log(target.name, ' is used');

};

}

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:

@Injectable()

export class DrawerService { .....

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.

Cheers!

Liron

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