# Lets learn Dependency Injection

— 7 min

I believe a lot in the saying “learning by doing”, and often the best thing I can do to better understand a specific problem, topic, library or paradigm, is to actually implement it myself.

Often its enough to only think about how I would implement it, but sometimes its good to also write it down, so that’s what I will do here.

So this specific journey started already a few month ago, when at my previous job, we started to embrace DI (dependency injection), more specifically in the form of nestjs. The specific question I wanted answered was: Why does nestjs come with its own module system while JS already has modules, and the nestjs modules, depending on your project size only ever have one Service/Provider in it. The concept didn’t immediately click for me, I only got it after I thought about how I would implement a DI solution myself.

So lets go!

Actually the concept behind DI is very simple. In my own words, what it does is to decouple the what from the how.

That is also how we can think about the moving parts. The central part in DI is called the Container. What you do is ask the container to give you the thing you want, the what, which essentially boils down to just a type. This is most commonly the type of your service, but you can also use it with primitives such as string or number if you wan’t to manage configuration via DI.

typedi calls this a Token:

// The class is empty, its only purpose is to hold the type T, the **what**.
class Token<T> {}

interface Service {
foo: string;
}

// Define some specific things you want to expose.
const MyConfig = new Token<string>();
const MyService = new Token<Service>();

But how do we actually construct the things that we want, the how? Really, the DI container itself does not really need to know itself how things are created. It just delegates this to any kind of function that does so, which is called the Provider.

type Provider<T> = (container: Container) => T;

In my example, I want to keep things as simple as possible from the container point of view, which means it is the responsibility of the Provider to:

1. initialize any dependency and
2. cache/memoize things. Thus, we arrive at this very simple Container:
class Container {
/** The registry holds the Providers keyed by Tokens. */
private registry = new Map<Token<any>, Provider<any>>();

/** Register a new Provider for a Token, the **how**. */
public register<T>(token: Token<T>, provider: Provider<T>): this {
this.registry.set(token, provider);
return this;
}

/** This will give you **what** you want, you don’t need to care **how**. */
public get<T>(token: Token<T>): T {
return this.registry.get(token)!(this); // NOTE: this may throw!
}
}

This is essentially a very simple but working DI Container in 12 lines of code! Lets use it!

class ConcreteService {
constructor(public foo: string) {}
}

const container = new Container();

// We can use a static value
container.register(MyConfig, () => "my config value");

// Here, our Provider constructs a new value matching the Service interface,
// and uses the DI container to get any dependency value.
// Again, neither the DI container itself nor the user does not need to care.
container.register(
MyService,
// If we want to have a singleton, we can just wrap this function with some
// kind of memoize, which is left as an excercise for the reader.
container => new ConcreteService(container.get(MyConfig))
);

// This will lazily create a new instance:
const myService = container.get(MyService);

You can also play with the complete example in the TypeScript Playground.

# # Making it more useful

Obviously, the example is optimized for simplicity and has some obvious problems:

• As noted in the comment, using get without previously register-ing a Provider will throw.
• It will run into infinite recursion when you have circular dependencies. I would argue to avoid circular dependencies in general. They work only in very specific circumstances and will blow up and burn your house down when you don’t take very good care.
• We can easily make this async as well.
• It might be a good idea to actually bake more sophisticated knowledge about dependencies into the Container itself, to optimize your dependency graph.
• The example is also completely missing things like scopes and inheritance.
• You might want to be able to use a Service both as Token and as Provider.

But even implemented like this, it very clearly highlights the main selling point of DI: Neither you as a programmer, nor any of your Providers need to know how other values are constructed. It just works. This makes it very easy to override one of your Providers to construct a mock object for your unit tests. Or to delegate to two different specific implementations of a service, depending on your configuration, etc.

# # Circling back to modules

Coming back to my original question about modules. We don’t see them in this very simple example. So lets think a bit about what happens when we start to scale this, when we have a lot more Providers to worry about, tens, or even hundreds of them.

We would need to call the register function of our Container for every single one, which gets tedious very quickly. Also, we want to both have some kind of encapsulation, and to not have to care about what specific Providers there are.

So how can we simplify that? Lets add an extremely simple Module definition, and extend our register method to deal with it:

// Tokens and Providers go hand-in-hand, let’s call it a Definition.
interface Definition<T> {
token: Token<T>;
provider: Provider<T>;
}

// A Module is essentially just a list of definitions.
type Module = Array<Definition<any>>;

class Container {
public register<T>(token: Token<T>, provider: Provider<T>): this;
public register(module: Module): this;
public register<T>(modOrToken: Module | Token<T>, provider?: Provider<T>) {
if (Array.isArray(modOrToken)) {
for (const def of modOrToken) {
this.registry.set(def.token, def.provider);
}
} else {
this.registry.set(token, provider);
}
return this;
}
}

// And then we can create and use a Module:
const ConfigModule = [
{ token: MyConfig, provider: () => "module config value" }
];
container.register(ConfigModule);

Here is another Playground Link.

There you go! A Module here is just an opaque set of Providers. Again, the benefit is that you don’t need to care about what is actually in it.

You can use it to group multiple configuration values together, or to mock all the things at once.

There is one last pitfall though: we only have one registry per container, and the way it is implemented mains that whatever you get now depends on the order in which you register things. And there could be potentially nasty surprises depending on your modules, because well, modules are supposed to be opaque. In some more complex examples you would still have to manually optimize the order of things.

Anyway, this is it. Actually writing this blog post showed me even more how simple the basic concepts behind DI actually are!

It does get more complicated if you want a richer API, which is async, supports circular dependencies (please don’t), and if you want to handle dependencies and memoization in the Container itself.