TypeScript’s compile-time type system changes all that, allowing continuous development of complex projects. It has brought back design patterns such as dependency injection, proper typing and passing of dependencies during object construction, which promotes more structured programming and helps write tests without the need for patching.
In this article, we’ll review five containerized dependency injection tools for writing dependency injection systems in TypeScript!
To study this article, you should be familiar with the following concepts.
- Inversion of Control (IoC): a design pattern that states that frameworks should call user-state code instead of user-state 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 support composition and can be wrapped around classes, functions, methods, accessors, properties, and parameters
- Decorator metadata: a way to store language structure configuration at runtime by using decorator definition targets
Explicitly injecting dependencies
Interfaces allow developers to separate abstract requirements from the actual implementation, which is very helpful when writing tests. Note that interfaces only define functionality, not dependencies. Finally, interfaces do not leave runtime traces, but classes do.
Let’s consider three example interfaces.
Logger interface abstracts synchronous logging, while the generic
FileSystem interface abstracts file CRUD operations. Finally, the
SettingsService interface provides a business logic abstraction for settings management.
We can infer that any implementation of
SettingsService depends on some implementation of the
FileSystem interfaces. For example, we could create a
ConsoleLogger class to print logs to console output, a
LocalFileSystem to manage files on the local disk, or a
SettingsTxtService class to write application settings to a file.
Dependencies can be passed explicitly using special functions.
SettingsTxtService This class does not depend on
ConsoleLogger or a similar implementation of
LocalFileSystem. Instead, it depends on the above interfaces
Logger and .
However, explicitly managing dependencies can cause problems for every DI container, since the interface does not exist at runtime.
Most injectable components of any system depend on other components. You should always be able to graph them, and the graph of a well-thought-out system will be loop-free. In my experience, circular dependencies are a code error, not a pattern.
The more complex the project, the more complex the dependency graph will be. In other words, managing dependencies explicitly does not scale well. We can solve this problem by automating dependency management, which makes it implicit. For this, we need a DI container.
Dependency Injection Container
The DI container requires the following.
ConsoleLoggerclass association with the
- the association of the
LocalFileSystemclass with the interface
- Correlation of
Binding a specific type or class to a specific interface at runtime can happen in two ways.
- specifying the name or token to bind the implementation to it
- elevating the interface to an abstract class and allowing the latter to leave a runtime trace
For example, we can use the container’s API to explicitly declare
ConsoleLogger that the class is associated with a
logger token. Alternatively, we can use a class-level decorator that accepts the name of the token as its argument. The decorator will then use the container’s API to register the binding.
Logger interface becomes an abstract class, we can apply class-level decorators to it and all its derived classes. When doing so, the decorator will call the container’s API to keep track of the runtime associations.
Dependencies can be resolved at runtime in two ways.
- Passing all dependencies during object construction
- Passing all dependencies after object construction using setter and getter
We will focus on the first option. the DI container is responsible for instantiating and maintaining the lifecycle of each component. Therefore, the container needs to know where to inject dependencies.
We have two ways to provide this information.
- using a constructor parameter decorator that can call the DI container API
- use the DI container’s API directly to inform it of the dependencies
Although decorators and metadata (such as Reflect API) are experimental features, they reduce the overhead when using the DI containers to reduce overhead.
Dependency Injection Containers Overview
Now, let’s look at five popular dependency injection containers. Note that the order used in this tutorial reflects how DI evolved as a pattern when it was applied to the TypeScript community.
The focus of the Typed Inject project is type safety and explicitness. It uses neither decorators nor decorator metadata, opting instead to declare dependencies manually. It allows the existence of multiple DI containers and the dependencies are qualified as single instance or transient objects.
The following code snippet outlines the conversion from contextual DI (shown in the previous code snippet) to typed injected DI.
TypedInjectFileSystem classes serve as concrete implementations of the required interfaces. Type binding is defined at the class level by listing object dependencies using the
inject static variable.
The following code snippet demonstrates all the major container operations in the Typed Inject environment.
createInjector function to instantiate the container and explicitly declare the token-to-class binding. Developers can use this
resolve function to access instances of the provided class. The injectable classes can be obtained using this
The InversifyJS project provides a lightweight DI container that creates application interfaces by tokenization. It uses decorators and decorator metadata for injection. However, binding the implementation to the interface still requires some manual work.
Dependency scoping is supported. The scope of an object can be a single instance object or a transient object, or it can be bound to a request. If necessary, developers can use separate DI containers.
The following code snippet demonstrates how to convert a contextual DI interface to use InversifyJS.
Following the official documentation, I created a mapping called
TYPES that contains all the tokens we will use for injection later on. I implemented the necessary interfaces, and
@injectable added class-level decorators for each interface. The arguments to the
InversifySettingsTxtService constructor use the
@injectable decorator to help the DI container resolve dependencies at runtime.
The code for the DI container is shown in the following code snippet.
InversifyJS uses a fluent interface pattern. the IoC container implements type binding between tokens and classes by explicitly declaring it in code. Getting an instance of a managed class requires only one call for proper conversion.
The TypeDI project is intended for simplicity by making use of decorated and decorated metadata. It supports single instance and transient object dependency scopes and allows the existence of multiple DI containers. You have two options for using TypeDI.
- Class-based injection
- Token-based injection
Class-based injection allows classes to be inserted by passing interface class relationships.
Each class uses the class-level
@Service decorator. The
global option means that all classes will be instantiated globally as a singleton. The class constructor parameter
TypeDiSettingsTxtService explicitly declares that it requires an instance of the
TypeDiLogger class and a
Once we have declared all the dependencies, we can use the TypeDI container as shown below.
Token-based Injection in TypeDI
Token-based injection uses tokens as intermediaries to bind interfaces to their implementations. The only change over class-based injections is the declaration of the
@Inject decoration that uses the appropriate token for each structural parameter.
We have to construct instances of the classes we need and connect them to the container.
The TSyringe project is a DI container maintained by Microsoft. It is a versatile container that supports almost all standard DI container features, including resolving circular dependencies. Similar to TypeDI, TSyringe supports both class-based and token-based injection.
Class-based injection in Tsyringe
Developers must use Tsyringe’s class-level decorators to mark target classes. In the following code snippet, we use the
The Tsyringe container can then automatically resolve the dependencies.
Token-based injection in Tsyringe
Similar to other libraries, TSyringe requires programmers to use constructor parameter decorators for token-based injection.
After declaring the target class, we can register a token class tuple with an associated lifecycle. In the following code snippet, I am using a singleton case.
NestJS is a framework that uses custom DI containers at the bottom. NestJS can be run as a standalone application as a wrapper for its DI container. It uses decorators and their metadata for injection. Scope is allowed and you can choose from single instance, transient object or request bound object.
The following code snippet includes a demonstration of NestJS functionality, starting with the declaration of the core class.
In the above code block, all target classes are marked with
@Injectable decorators. Next, we define the core class of the
AppModule application and specify its dependencies
Finally, we can create application contexts and get instances of the above classes.
In this tutorial, we introduced what a dependency injection container is and why you should use it. We then explored five different TypeScript dependency injection containers and walked through examples of how to use each one.
Now that TypeScript is a dominant programming language, using established design patterns such as dependency injection can help developers make the transition from other languages.