Swatinem Blog Resume

Lets learn Dependency Injection

— 5 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 `Provider`s keyed by `Token`s. */
  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:

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.