What is a Dependency Injection Container for TypeScript?
Dependency Injection (DI) is a design pattern where an object receives its dependencies from the outside rather than creating them itself. A DI container is a tool that manages the creation and injection of these dependencies automatically. In TypeScript, thanks to static typing, DI containers are especially effective: they ensure type safety and simplify testing.
The main task of the container is to bind interfaces to their concrete implementations and resolve dependency chains. This allows you to write loosely coupled, modular code that is easy to maintain and extend.
Why do you need a DI container in TypeScript?
- Reduced code coupling — classes depend only on abstractions (interfaces), not on concrete implementations.
- Simplified testing — dependencies can be easily replaced with mocks or stubs.
- Lifecycle management — the container can create singletons, transient objects, or objects with a specific scope.
- Automatic dependency resolution — the container builds the dependency tree itself, freeing the developer from manual object construction.
Popular DI Containers for TypeScript
Let's look at the two most common libraries: InversifyJS and TSyringe. Both support TypeScript but have different approaches.
1. InversifyJS
InversifyJS is a powerful IoC container with decorators. It requires explicit declaration of dependencies via the @injectable() and @inject() decorators. Suitable for large projects with a clear architecture.
Installation
npm install inversify reflect-metadata --saveIn the tsconfig.json file, be sure to enable:
{ "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true }}Usage Example
import { Container, injectable, inject } from "inversify";import "reflect-metadata";
// Define an interface and a symbol for bindinginterface ILogger { log(message: string): void;}const TYPES = { ILogger: Symbol.for("ILogger"), IUserService: Symbol.for("IUserService"),};
// Logger implementation@injectable()class ConsoleLogger implements ILogger { log(message: string): void { console.log(`[LOG]: ${message}`); }}
// Service depending on the logger@injectable()class UserService { constructor(@inject(TYPES.ILogger) private logger: ILogger) {}
createUser(name: string): void { this.logger.log(`User ${name} created`); }}
// Container configurationconst container = new Container();container.bind<ILogger>(TYPES.ILogger).to(ConsoleLogger).inSingletonScope();container.bind<UserService>(TYPES.IUserService).to(UserService);
// Usageconst userService = container.get<UserService>(TYPES.IUserService);userService.createUser("Alexey");2. TSyringe
TSyringe is a lightweight DI container from Microsoft. It uses decorators and automatic resolution via the constructor. Ideal for medium and small projects.
Installation
npm install tsyringe reflect-metadata --saveThe tsconfig.json configuration is similar to InversifyJS.
Usage Example
import { container, injectable, singleton } from "tsyringe";import "reflect-metadata";
// Register a class as a singleton@singleton()class DatabaseConnection { connect(): void { console.log("Database connection established"); }}
// Class with automatic dependency injection@injectable()class UserRepository { constructor(private db: DatabaseConnection) {}
save(user