Simplifying Dependency Injection in TypeScript with Injected-ts

A simple DI (Dependency Injection) library for typescript

Nash
3 min readJun 23, 2023

Introduction

Dependency injection is a powerful technique that promotes modular and loosely coupled code by managing the dependencies of an application and facilitating their injection into classes. In TypeScript, implementing dependency injection can sometimes be cumbersome and time-consuming. The main drawback of existing library is how they are coupled to your code, by using decorators, or force you to add some code in your production code to enable DI.

However, with the advent of Injected-ts, a lightweight dependency injection library, managing dependencies in TypeScript has become easier and more efficient. In this article, we will explore the motivations behind using Injected-ts, delve into its inner workings, and demonstrate how to leverage its features to simplify dependency injection in TypeScript projects.

The code is open source and available here: https://github.com/remihenache/injected-ts

Why Use Injected-ts?

  1. Modularity and Maintainability: Injected-ts enables you to break down your code into smaller, manageable modules with clear dependencies. By decoupling dependencies from the classes that use them, you can easily replace or modify implementations without affecting other parts of the codebase.
  2. Testability: With dependency injection, you can easily substitute dependencies with mock objects or test doubles during unit testing. This facilitates more thorough testing, improves test coverage, and enhances overall code quality.
  3. Flexibility: Injected-ts provides a flexible and intuitive API for registering and resolving dependencies. It supports various lifecycle options, constructor parameter injection, interface injection, and scope management. This flexibility allows you to tailor the dependency injection approach to the specific needs of your project.

The Inner Workings of Injected-ts

At the core of Injected-ts are two main classes: ServiceCollection and ServiceProvider. Let's explore how they work together to simplify dependency injection in TypeScript.

ServiceCollection:

  • The ServiceCollection class serves as a container for registering dependencies. It provides methods such as addScoped, addTransient, and addSingleton to register dependencies with different lifecycles.
  • When registering a dependency, you can use the fluent API provided by ServiceCollection to specify the dependency type and its associated configuration, such as constructor dependencies.
  • The build method of ServiceCollection creates an instance of ServiceProvider with the registered dependencies.

ServiceProvider:

  • The ServiceProvider class is responsible for resolving dependencies and managing their lifecycles.
  • By calling the get method on ServiceProvider with the appropriate dependency identifier, you can resolve and retrieve an instance of the requested dependency.
  • ServiceProvider supports scoped lifecycles, allowing you to create and manage scopes using the startScope and endScope methods. This enables you to control the lifetime of dependencies within specific scopes.

Using Injected-ts in Practice

To demonstrate how to use Injected-ts, let’s consider an example scenario where we have a UserService that depends on a UserRepository. Here's how we can implement it using Injected-ts:

Create a ServiceCollection instance:

import { ServiceCollection } from 'injected-ts';
const services = new ServiceCollection();

Register dependencies:

services.addScoped<UserRepository>((builder) =>
builder.fromType(UserRepository)
);

services.addScoped<UserService>((builder) =>
builder.fromType(UserService).withDependencies(UserRepository)
);ty

Build and use the ServiceProvider:

const serviceProvider = services.build();

const userRepository = serviceProvider.get<UserRepository>(UserRepository);
const userService = serviceProvider.get<UserService>(UserService);

// Use the dependencies
userService.registerUser('John Doe');

Lifecycles

The library can handle three lifecycle:

  • Singleton: one instance for all the application
  • Transient: one instance each time you inject it
  • Scoped: one instance by scope

You can create your own custom lifecycle if need by implementing the Lifcycle interface, then use the add method.

class CustomLifecycle implements Lifecycle {
// Implement the lifecycle methods
}

services.add<MyService>(new CustomLifecycle(), (builder) =>
builder.fromType(MyService).withDependencies(Dependency1, Dependency2)
);

Use the ServiceProvider inside a class or a factory

Injected-ts provides the possibility to access the service provider instance by injecting it inside a class or a factory:

  • Injection
class MyService {
constructor(serviceProvider: ServiceProvider) {}
public doSomething() {
const anotherService = this.serviceProvider.get<AnotherService>(AnotherService.name);
// Do something with the service
}
}
  • Factory
services.addSingleton<ServiceDependency>((builder) =>
builder.fromType(ServiceDependency.name).withDependencies(TypeDependency)
);
services.addSingleton<MyService>((builder) =>
builder.fromName('MyService').useFactory((serviceProvider) => new MyService("Some value", serviceProvider.get<ServiceDependency>(ServiceDependency.name)))
);

Conclusion

Injected-ts is a valuable tool for simplifying dependency injection in TypeScript projects. By leveraging its features, such as the fluent API for dependency registration, flexible lifecycle management, and scope support, you can enhance

--

--