Dependency Injection in TypeScript
Tags: typescript, tutorial, di, solid
Published: 2021-05-03 14:47:30

The D letter in SOLID is the Dependency Inversion principle. It helps to decouple modules from each other so that you can easily swap one part of the code for another.

One of the techniques that helps to follow this principle is Dependency Injection.

> This post was inspired by [Sasha Bespoyasov's article](https://bespoyasov.ru/blog/di-ts-in-practice/) and is partially a translation of it.

## What Are Dependencies?

For ease of reference, we will define a dependency as any module that is used by our module.

Let's look at a function that takes two numbers and returns a random number in a range:

```typescript
const getRandomInRange = (min: number, max: number): number =>
  Math.random() * (max - min) + min;
```
The function depends on two arguments: `min` and `max`.

But you can see that the function depends not only on the arguments, but also on the `Math.random` function. If `Math.random` is not defined, our `getRandomInRange` function won't work either. That is, `getRandomInRange` depends on the functionality of another module. Therefore `Math.random` is also a dependency.

Let's explicitly pass the dependency through arguments:

```typescript
const getRandomInRange = (
  min: number,
  max: number,
  random: () => number,
): number => random() * (max - min) + min;
```

Now the function uses not only two numbers, but also a `random` function that returns number. We will call the `getRandomInRange` like this:

```typescript
const result = getRandomInRange(1, 10, Math.random);
```

To avoid passing `Math.random` all the time, we can make it the default value for the last argument.

```typescript
const getRandomInRange = (
  min: number,
  max: number,
  random: () => number = Math.random
): number => random() * (max - min) + min;
```

**This is the primitive implementation of the Dependency Inversion.** We pass to our module all the dependencies it needs to work.

## Why Is It Needed?

Really, why put `Math.random` in the argument and use it from there? What's wrong with just using it inside a function? There are two reasons for that.

### Testability

When all dependencies are explicitly declared, the module is easier to test. We can see what needs to be prepared to run a test. We know which parts affect the operation of this module, so we can replace them with some simple implementation or mock implementation if needed.

Mock implementations make testing a lot easier, and sometimes you can't test anything at all without them. As in the case of our function `getRandomInRange`, we cannot test the final result it returns, because it is.... random.

```typescript
/*
 * We can create a mock function
 * that will always return 0.1 instead of a random number:
 */
const mockRandom = () => 0.1;

/* Next, we call our function by passing the mock object as its last argument: */
const result = getRandomInRange(1, 10, mockRandom);

/*
 * Now, since the algorithm within the function is known and deterministic,
 * the result will always be the same:
 */
console.log(result === 1); // -> true
```

### Replacing an Dependency With Another Dependency

Swapping dependencies during tests is a special case. In general, we may want to swap one module for another for some other reason.

If the new module behaves the same as the old one, we can replace one with the other:

```typescript
const otherRandom = (): number => {
  /* Another implementation of getting a random number... */
};

const result = getRandomInRange(1, 10, otherRandom);
```

But can we guarantee that the new module will behave the same way as the old one? Yes we can, because we use the `() => number` argument type. This is why we use TypeScript, not JavaScript. Types and interfaces are links between modules.

## Dependencies on Abstractions


At first glance, this may seem like overcomplication. But in fact, with this approach:

- modules become less dependent on each other;
- we are forced to design the behavior before we start writing the code.

When we design behavior in advance, we use abstract conventions. Under these conventions, we design our own modules or adapters for third-party ones. This allows us to replace parts of the system without having to rewrite it completely.

This especially comes in handy when the modules are more complex than in the examples above.

## Dependency Injection

Let's write yet another counter. Our counter can increase and decrease. And it also logs the state of.

```typescript
class Counter {
  public state: number = 0;

  public increase(): void {
	this.state += 1;
	console.log(`State increased. Current state is ${this.state}.`);
  }

  public decrease(): void {
	this.state -= 1;
	console.log(`State decreased. Current state is ${this.state}.`);
  }
}
```

Here we run into the same problem as before — we use not only the internal state of the class instance, but also another module — `console`. We have to **inject** this dependence.

If in functions we passed dependencies through arguments, then in classes we will inject dependencies through the constructor.

Our `Counter` class uses the log method of the console object. This means that the class needs to pass some object with the `log` method as a dependency. It doesn't have to be `console` — we keep in mind the testability and replaceability of modules.

```typescript
interface Logger {
  log(message: string): void;
}

class Counter {
  constructor(
    private logger: Logger,
  ) {}

  public state: number = 0;

  public increase(): void {
	this.state += 1;
	this.logger.log(`State increased. Current state is ${this.state}.`);
  }

  public decrease(): void {
	this.state -= 1;
	this.logger.log(`State decreased. Current state is ${this.state}.`);
  }
}
```

We will then create instances of this class as follows: 

```typescript
const counter = new Counter(console);
```

And if we want to replace `console` with something else, we just have to make sure that the new dependency implements the `Logger` interface:

```typescript
const alertLogger: Logger = {
  log: (message: string): void => {
	alert(message);
  },
};

const counter = new Counter(alertLogger);
```

## Automatic Injections and DI Containers

Now the class doesn't use implicit dependencies. This is good, but this injection is still awkward: you have to add references to objects by hand every time, try not to mix up the order if there are several of them, and so on…

This should be handled by a special thing — a DI Container. In general terms, a DI Container is a module that only provides dependencies to other modules.

The container knows which dependencies which module needs, creates and injects them when needed. We free objects from the obligation to keep track of their dependencies, the control goes elsewhere, as the letters S and D in SOLID imply.

The container will know which objects with which interfaces are required by each of the modules. It will also know which objects implement those interfaces. And when creating objects that depend on such interfaces, the container will create them and access them automatically.

## Automatic Injections in Practice

We will use the [Brandi DI Container](https://brandi.js.org/), which does exactly what we described above.

Let's begin by declaring the `Logger` interface and creating its `ConsoleLogger` implementation:

```typescript
/* Logger.ts */

export interface Logger {
  log(message: string): void;
}

export class ConsoleLogger implements Logger {
  public log(message: string): void {
    console.log(message)
  }
}
```

Now we need to get to know the tokens. Since we are compiling TypeScript into JavaScript, there will be no interfaces and types in our code. Brandi uses tokens to bind dependencies to their implementations in JavaScript runtime.

```typescript
/* tokens.ts */

import { token } from 'brandi';

import { Logger } from './Logger';
import { Counter } from './Counter';

export const TOKENS = {
  logger: token<Logger>('logger'),
  counter: token<Counter>('counter'),
}; 
```

The code above mentions the `Counter`. Let's see how dependencies are injected into it:

```typescript
/* Counter.ts */

import { injected } from 'brandi';

import { TOKENS } from './tokens';
import { Logger } from './Logger';

export class Counter {
  constructor(
	private logger: Logger,
  ) {}

  /* Other code... */
}

injected(Counter, TOKENS.logger);
```

We use the `injected` function to specify by which token the dependency should be injected.

Since tokens are typed, it is not possible to inject a dependency with a different interface, this will throw an error at compile time.

Finally, let's configure the container:

```typescript
/* container.ts */

import { Container } from 'brandi';

import { TOKENS } from './tokens';
import { ConsoleLogger } from './logger';
import { Counter } from './counter';

export const container = new Container();

container
  .bind(TOKENS.logger)
  .toInstance(ConsoleLogger)
  .inTransientScope();

container
  .bind(TOKENS.counter)
  .toInstance(Counter)
  .inTransientScope();
```

We bound tokens to their implementation.

Now when we get an instance from the container, its dependencies will be injected automatically:

```typescript
/* index.ts */

import { TOKENS } from './tokens';
import { container } from './container';

const counter = container.get(TOKENS.counter);

counter.increase() // -> State increased. Current state is 1.
```

### What's the `inTransientScope()`?

Transient is the kind of instance lifecycle that the container will create.

- `inTransientScope()` — a new instance will be created each time it is gotten from the container;
- `inSingletonScope()` — each getting will return the same instance.

Brandi also allows you to create instances in [container](https://brandi.js.org/reference/binding-scopes#incontainerscope) and [resolution](https://brandi.js.org/reference/binding-scopes#inresolutionscope) scopes.

## Benefits of The Container


The first benefit is that we can change the implementation in all modules with a single line. This way we achieve the control inversion that the last letter of SOLID talks about.

For example, if we want to change the `Logger` implementation in all the places that depend on this interface, we only need to change the binding in the container:

```typescript
/* The new implementation. */
class AlertLogger implements Logger {
  public log(message: string): void {
    alert(message);
  }
}

/*
 * With this implementation we replace the old `ConsoleLogger`.
 * This only needs to be done in one place, at the token binding:
 */
container
  .bind(TOKENS.logger)
  .toInstance(AlertLogger)
  .inTransientScope();
```

In addition, we don't pass dependencies by hand, we don't need to follow the order of enumeration in constructors, the modules become less coupled.

## Should You Use DI?

Take some time to understand the potential benefits and tradeoffs of using DI. You will write some infrastructural code, but in return your code will be less coupled, more flexible and easier to test.