Top 5 TypeScript dependency injection containers
Tags: typescript, webdev
Published: 2021-07-29 13:54:16

**Written by [Gregory Pabian] (https://blog.logrocket.com/author/gregorypabian/)** ✏️

As a software developer who started my career with Java, I had issues during my transition to JavaScript. The original environment lacked a static type system and had virtually no support for containerized dependency injection, causing me to write code that was prone to obvious bugs and barely testable.

TypeScript’s compile-time type system changed it all, allowing for the continuous development of complex projects. It enabled the reemergence of design patterns like dependency injection, typing and passing dependencies correctly during object construction, which promotes more structured programming and facilitates writing tests without monkey patching.

In this article, we’ll review five containerized dependency injection tools for writing dependency injection systems in TypeScript. Let’s get started!

### Prerequisites

To follow along with this article, you should be familiar with the following concepts:

*   Inversion of Control (IoC): a design pattern that stipulates frameworks should call userland code, instead of userland code calling library code
*   Dependency injection (DI): a variant of IoC in which objects receive other objects as dependencies instead of constructors or setters
*   Decorators: functions that enable composition and are wrappable around classes, functions, methods, accessors, properties, and parameters
*   Decorator metadata: a way to store configuration for language structures in runtime by using decorators to define targets

## Explicitly injecting dependencies

Interfaces allow developers to decouple abstraction requirements from actual implementation, which helps tremendously in writing tests. Note that interfaces define only functionality, not dependencies. Lastly, interfaces leave no runtime trace, however, classes do.

Let’s consider three example interfaces:

```typescript
export interface Logger {
    log: (s: string) => void;
}

export interface FileSystem<D> {
    createFile(descriptor: D, buffer: Buffer): Promise<void>;
    readFile(descriptor: D): Promise<Buffer>;
    updateFile(descriptor: D, buffer: Buffer): Promise<void>;
    deleteFile(descriptor: D): Promise<void>;
}

export interface SettingsService {
    upsertSettings(buffer: Buffer): Promise<void>;
    readSettings(): Promise<Buffer>;
    deleteSettings(): Promise<void>;
}
```

The `Logger` interface abstracts synchronous logging away, while the generic `FileSystem` interface abstracts file CRUD operations away. Finally, the `SettingsService` interface provides a business-logic abstraction over settings management.

We can infer that any implementation of the `SettingsService` depends on some implementations of the `Logger` and the `FileSystem` interfaces. For example, we could create a `ConsoleLogger` class to print logs to the console output, create a `LocalFileSystem` to manage the files on the local disc, or create a `SettingsTxtService` class to write application settings to a `settings.txt` file.

Dependencies can be passed explicitly using special functions:

```typescript
export class ConsoleLogger implements Logger {
    // ...
}

export class LocalFileSystem implements FileSystem<string> {
    // ...
}

export class SettingsTxtService implements SettingsService {
    protected logger!: Logger;
    protected fileSystem!: FileSystem<string>;

    public setLogger(logger: SettingsTxtService["logger"]): void {
        this.logger = logger;
    }

    public setFileSystem(fileSystem: SettingsTxtService["fileSystem"]): void {
        this.fileSystem = fileSystem;
    }

    // ...
}

const logger = new ConsoleLogger();
const fileSystem = new LocalFileSystem();
const settingsService = new SettingsTxtService();

settingsService.setLogger(logger);
settingsService.setFileSystem(fileSystem);
```

The `SettingsTxtService` class does not depend on implementations like `ConsoleLogger` or `LocalFileSystem`. Instead, it depends on the aforementioned interfaces, `Logger` and `FileSystem<string>`.

However, explicitly managing dependencies poses a problem for every DI container because interfaces do not exist in runtime.

### Dependency graphs

Most injectable components of any system depend on other components. You should be able to draw a graph of them at any time, and the graph of a well-thought-out system will be acyclic. Based on my experience, cyclic dependencies are a code smell, not a pattern.

The more complex a project becomes, the more complex the dependency graphs become. In other words, explicitly managing dependencies does not scale well. We can remedy this by automating dependency management, which makes it implicit. To do so, we’ll need a DI container.

## Dependency injection containers

A DI container requires the following:

*   the association of the `ConsoleLogger` class with the `Logger` interface
*   the association of the `LocalFileSystem` class with the `FileSystem<string>` interface
*   the dependency of the `SettingsTxtService` on both the `Logger` and the `FileSystem<string>` interfaces

## Type bindings

Binding a specific type or class to a specific interface in runtime can occur in two ways:

*   specifying a name or token that binds the implementation to it
*   promoting an interface to an abstract class and allowing the latter to leave a runtime trace

For example, we could explicitly state that the `ConsoleLogger` class is associated with the `logger` token using the container's API. Alternately, we could use a class-level decorator that accepts the token name as its parameter. The decorator would then use the container's API to register the binding.

If the `Logger` interface becomes an abstract class, we could apply a class-level decorator to it and all of its derived classes. In doing so, the decorators would call the container's API to track the associations in runtime.

## Resolving dependencies

Resolving dependencies in runtime is possible in two ways:

*   passing all dependencies during object construction
*   passing all dependencies using setters and getters after object construction

We’ll focus on the first option. A DI container is responsible for instantiating and maintaining every component's lifecycle. Therefore, the container needs to know where to inject dependencies.

We have two ways to provide this information:

1.  using constructor parameter decorators that are capable of calling the DI container's API
2.  using the DI container's API directly to inform it about the dependencies

Although decorators and metadata, like the [Reflect API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect), are experimental features, they reduce overhead when using DI containers.

## Dependency injection container overview

Now, let’s look at five popular containers for dependency injection. Note that the order used in this tutorial reflects how DI evolved as a pattern while being applied in the TypeScript community.

### Typed Inject

The [Typed Inject](https://github.com/nicojs/typed-inject) project focuses on type safety and explicitness. It uses neither decorators nor decorator metadata, opting instead for manually declaring dependencies. It allows for multiple DI containers to exist, and dependencies are scoped either as singletons or as transient objects.

The code snippet below outlines the transition from the contextual DI, which was shown in previous code snippets, to the Typed Inject DI:

```typescript
export class TypedInjectLogger implements Logger {
    // ...
}
export class TypedInjectFileSystem implements FileSystem<string> {
    // ...
}

export class TypedInjectSettingsTxtService extends SettingsTxtService {
    public static inject = ["logger", "fileSystem"] as const;

    constructor(
        protected logger: Logger,
        protected fileSystem: FileSystem<string>,
    ) {
        super();
    }
}
```

The `TypedInjectLogger` and `TypedInjectFileSystem` classes serve as concrete implementations of the required interfaces. Type bindings are defined on the class-level by listing object dependencies using `inject`, a static variable.

The following code snippet demonstrates all major container operations within the Typed Inject environment:

```typescript
const appInjector = createInjector()
    .provideClass("logger", TypedInjectLogger, Scope.Singleton)
    .provideClass("fileSystem", TypedInjectFileSystem, Scope.Singleton);

const logger = appInjector.resolve("logger");
const fileSystem = appInjector.resolve("fileSystem");
const settingsService = appInjector.injectClass(TypedInjectSettingsTxtService);
```

The container is instantiated using the `createInjector` functions, with token-to-class bindings declared explicitly. Developers can access instances of provided classes using the `resolve` function. Injectable classes can be obtained using the `injectClass` method.

### InversifyJS

The [InversifyJS](https://github.com/inversify/InversifyJS) project provides a lightweight DI container that uses interfaces created through tokenization. It uses decorators and decorators’ metadata for injections. However, some manual work is still necessary for binding implementations to interfaces.

Dependency scoping is supported. Objects can be scoped either as singletons or transient objects, or bound to a request. Developers can use separate DI containers if necessary.

The code snippet below demonstrates how to transform the contextual DI interface to use InversifyJS:

```typescript
export const TYPES = {
    Logger: Symbol.for("Logger"),
    FileSystem: Symbol.for("FileSystem"),
    SettingsService: Symbol.for("SettingsService"),
};

@injectable()
export class InversifyLogger implements Logger {
    // ...
}

@injectable()
export class InversifyFileSystem implements FileSystem<string> {
    // ...
}

@injectable()
export class InversifySettingsTxtService implements SettingsService {
    constructor(
        @inject(TYPES.Logger) protected readonly logger: Logger,
        @inject(TYPES.FileSystem) protected readonly fileSystem: FileSystem<string>,
    ) {
        // ...
    }
}
```

Following the [official documentation](https://inversify.io), I created a map called `TYPES` that contains all the tokens we’ll use later for injection. I implemented the necessary interfaces, adding the class-level decorator `@injectable` to each. The parameters of the `InversifySettingsTxtService`  constructor use the `@inject` decorator, helping the DI container to resolve dependencies in runtime.

The code for the DI container is seen in the code snippet below:

```typescript
const container = new Container();
container.bind<Logger>(TYPES.Logger).to(InversifyLogger).inSingletonScope();
container.bind<FileSystem<string>>(TYPES.FileSystem).to(InversifyFileSystem).inSingletonScope();
container.bind<SettingsService>(TYPES.SettingsService).to(InversifySettingsTxtService).inSingletonScope();

const logger = container.get<InversifyLogger>(TYPES.Logger);
const fileSystem = container.get<InversifyFileSystem>(TYPES.FileSystem);
const settingsService = container.get<SettingsTxtService>(TYPES.SettingsService);
```

InversifyJS uses the fluent interface pattern. The IoC container achieves type binding between tokens and classes by declaring it explicitly in code. Getting instances of managed classes requires only one call with proper casting.

### TypeDI

The [TypeDI](https://github.com/typestack/typedi) project aims for simplicity by leveraging decorators and decorator metadata. It supports dependency scoping with singletons and transient objects and allows for multiple DI containers to exist. You have two options for working with TypeDI:

*   class-based injections
*   token-based injections

#### Class-based injections

Class-based injections allow for the insertion of classes by passing interface-class relationships:

```typescript
@Service({ global: true })
export class TypeDiLogger implements Logger {}

@Service({ global: true })
export class TypeDiFileSystem implements FileSystem<string> {}

@Service({ global: true })
export class TypeDiSettingsTxtService extends SettingsTxtService {
    constructor(
        protected logger: TypeDiLogger,
        protected fileSystem: TypeDiFileSystem,
    ) {
        super();
    }
}
```

Every class uses the class-level `@Service` decorator. The `global` option means all classes will be instantiated as singletons in the global scope. The constructor parameters of the `TypeDiSettingsTxtService` class explicitly state that it requires one instance of the `TypeDiLogger` class and one of the `TypeDiFileSystem` class.

Once we have declared all dependencies, we can use TypeDI containers as follows:

```typescript
const container = Container.of();

const logger = container.get(TypeDiLogger);
const fileSystem = container.get(TypeDiFileSystem);
const settingsService = container.get(TypeDiSettingsTxtService);
```

#### Token-based injections in TypeDI

Token-based injections bind interfaces to their implementations using a token as an intermediary. The only change in comparison to class-based injections is declaring the appropriate token for each construction parameter using the `@Inject` decorator:

```typescript
@Service({ global: true })
export class TypeDiLogger extends FakeLogger {}

@Service({ global: true })
export class TypeDiFileSystem extends FakeFileSystem {}

@Service({ global: true })
export class ServiceNamedTypeDiSettingsTxtService extends SettingsTxtService {
    constructor(
        @Inject("logger") protected logger: Logger,
        @Inject("fileSystem") protected fileSystem: FileSystem<string>,
    ) {
        super();
    }
}
```

We have to construct the instances of the classes we need and connect them to the container:

```typescript
const container = Container.of();

const logger = new TypeDiLogger();
const fileSystem = new TypeDiFileSystem();

container.set("logger", logger);
container.set("fileSystem", fileSystem);

const settingsService = container.get(ServiceNamedTypeDiSettingsTxtService);
```

#### TSyringe

The [TSyringe](https://github.com/microsoft/tsyringe) project is a DI container maintained by Microsoft. It is a versatile container that supports virtually all standard DI container features, including resolving circular dependencies. Similar to TypeDI, TSyringe supports class-based and token-based injections.

#### Class-based injections in TSyringe

Developers must mark the target classes with TSyringe’s class-level decorators. In the code snippet below, we use the `@singleton` decorator:

```typescript
@singleton()
export class TsyringeLogger implements Logger {
    // ...
}

@singleton()
export class TsyringeFileSystem implements FileSystem {
    // ...
}

@singleton()
export class TsyringeSettingsTxtService extends SettingsTxtService {
    constructor(
        protected logger: TsyringeLogger,
        protected fileSystem: TsyringeFileSystem,
    ) {
        super();
    }
}
```

The TSyringe containers can then resolve dependencies automatically:

```typescript
const childContainer = container.createChildContainer();

const logger = childContainer.resolve(TsyringeLogger);
const fileSystem = childContainer.resolve(TsyringeFileSystem);
const settingsService = childContainer.resolve(TsyringeSettingsTxtService);
```

#### Token-based injections in TSyringe

Similar to other libraries, TSyringe requires programmers to use constructor parameter decorators for token-based injections:

```typescript
@singleton()
export class TsyringeLogger implements Logger {
    // ...
}

@singleton()
export class TsyringeFileSystem implements FileSystem {
    // ...
}

@singleton()
export class TokenedTsyringeSettingsTxtService extends SettingsTxtService {
    constructor(
        @inject("logger") protected logger: Logger,
        @inject("fileSystem") protected fileSystem: FileSystem<string>,
    ) {
        super();
    }
}
```

After declaring target classes, we can register token-class tuples with the associated lifecycles. In the code snippet below, I’m using a singleton:

```typescript
const childContainer = container.createChildContainer();

childContainer.register("logger", TsyringeLogger, { lifecycle: Lifecycle.Singleton });
childContainer.register("fileSystem", TsyringeFileSystem, { lifecycle: Lifecycle.Singleton });

const logger = childContainer.resolve<FakeLogger>("logger");
const fileSystem = childContainer.resolve<FakeFileSystem>("fileSystem");
const settingsService = childContainer.resolve(TokenedTsyringeSettingsTxtService);
```

### NestJS

[NestJS](https://nestjs.com) is a framework that uses a custom DI container under the hood. It is possible to run NestJS as a standalone application as a wrapper over its DI container. It uses decorators and their metadata for injections. Scoping is allowed, and you can choose from singletons, transient objects, or request-bound objects.

The code snippet below includes a demonstration of [NestJS](https://blog.logrocket.com/unit-testing-nestjs-applications-with-jest/) capabilities, starting from declaring the core classes:

```typescript
@Injectable()
export class NestLogger implements Logger {
    // ...
}

@Injectable()
export class NestFileSystem extends FileSystem<string> {
    // ...
}

@Injectable()
export class NestSettingsTxtService extends SettingsTxtService {
    constructor(
        protected logger: NestLogger,
        protected fileSystem: NestFileSystem,
    ) {
        super();
    }
}
```

In the code block above, all targeted classes are marked with the `@Injectable` decorator. Next, we defined the `AppModule`, the core class of the application, and specified its dependencies, `providers`:

```typescript
@Module({
    providers: [NestLogger, NestFileSystem, NestSettingsTxtService],
})
export class AppModule {}
```

Finally, we can create the application context and get the instances of the aforementioned classes:

```typescript
const applicationContext = await NestFactory.createApplicationContext(
    AppModule,
    { logger: false },
);

const logger = applicationContext.get(NestLogger);
const fileSystem = applicationContext.get(NestFileSystem);
const settingsService = applicationContext.get(NestSettingsTxtService);
```

## Summary

In this tutorial, we covered what a dependency injection container is, and why you would use one. We then explored five different dependency injection containers for TypeScript, learning how to use each with an example.

Now that TypeScript is a mainstream programming language, using established design patterns like dependency injection may help developers transition from other languages.

---

## [LogRocket] (https://logrocket.com/signup/): Full visibility into your web apps

[![LogRocket Dashboard Free Trial Banner] (https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)](https://logrocket.com/signup/)

[LogRocket] (https://logrocket.com/signup/) is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

[Try it for free] (https://logrocket.com/signup/).