TypeScript IoC Container
typescript-ioc is a lightweight, annotation-based dependency injection (DI) container specifically designed for TypeScript applications. It allows developers to manage dependencies using decorators like `@Inject`, simplifying object graph creation and promoting modular design. The library is currently stable at version 3.2.2, with the 3.x series being actively maintained through frequent patch and minor releases. Key differentiators include its small footprint, ease of use with TypeScript decorators, and broad environmental support including Node.js, browsers, and React Native. It provides features such as different scopes (Singleton, Request, Local), factory creation, and configuration capabilities, making it suitable for both simple and complex application architectures where inversion of control is desired. A notable change in the 3.x series was the discontinuation of support for ES5 runtimes, requiring modern JavaScript environments (ES6 or newer).
Common errors
-
TypeError: Cannot read properties of undefined (reading 'constructor')
cause `emitDecoratorMetadata` is not enabled in `tsconfig.json`, preventing TypeScript from emitting necessary type information for `typescript-ioc` to resolve dependencies.fixAdd `'emitDecoratorMetadata': true` to `compilerOptions` in your `tsconfig.json`. -
Error: Decorators are not enabled.
cause `experimentalDecorators` is not set to `true` in your `tsconfig.json`, which is required for all TypeScript decorators.fixAdd `'experimentalDecorators': true` to `compilerOptions` in your `tsconfig.json`. -
Error: Class or instance not registered in container
cause Attempted to `Container.get(MyInterface)` without a prior `Container.bind(MyInterface).to(MyImplementation)` or a class that is not decorated/registered correctly.fixFor interfaces, ensure an explicit `Container.bind(Interface).to(Implementation);` exists. For classes, ensure they are `@Inject`-able or bound.
Warnings
- breaking Version 3.0.0 and above of `typescript-ioc` removed support for ES5 runtimes. Projects must target ES6 or newer.
- gotcha TypeScript decorators are a required feature and need specific compiler options enabled to function correctly with `typescript-ioc`.
- gotcha When injecting an interface, `typescript-ioc` requires an explicit binding to a concrete implementation via `Container.bind(Interface).to(Implementation)`. It cannot infer the implementation from the interface type alone.
Install
-
npm install typescript-ioc -
yarn add typescript-ioc -
pnpm add typescript-ioc
Imports
- Inject
const Inject = require('typescript-ioc').Inject;import { Inject } from 'typescript-ioc'; - Container
import * as IoC from 'typescript-ioc'; const Container = IoC.Container;
import { Container } from 'typescript-ioc'; - Scope
import { ScopeType } from 'typescript-ioc';import { Scope } from 'typescript-ioc';
Quickstart
import { Inject, Container, Scope } from "typescript-ioc";
// Ensure your tsconfig.json has:
// "experimentalDecorators": true,
// "emitDecoratorMetadata": true,
// "target": "es6"
interface ILogger {
log(message: string): void;
}
@Scope(Scope.Singleton)
class ConsoleLogger implements ILogger {
private readonly timestamp: Date;
constructor() {
this.timestamp = new Date();
console.log(`ConsoleLogger instance created at ${this.timestamp.toISOString()}`);
}
log(message: string): void {
console.log(`[${this.timestamp.toISOString()}] ${message}`);
}
}
class DatabaseService {
@Inject
private logger!: ILogger; // '!' is TypeScript's definite assignment assertion
connect(): void {
this.logger.log("Attempting to connect to database...");
// Simulate database connection logic
}
}
class UserService {
@Inject
private dbService!: DatabaseService;
@Inject('APP_NAME') // Injecting a constant value defined by Container.bindName
private appName!: string;
createUser(username: string): void {
this.dbService.connect();
console.log(`[${this.appName}] Creating user: ${username}`);
// Simulate user creation logic
}
}
// Configure the container: bind an interface to an implementation
Container.bind(ILogger).to(ConsoleLogger);
// Bind a named constant value
Container.bindName('APP_NAME').to('MyAwesomeApp');
// Option 1: Get instance from the container to ensure all dependencies are resolved
const userServiceFromContainer = Container.get(UserService);
userServiceFromContainer.createUser("Alice");
// Option 2: Instantiate directly, and the container will inject dependencies
const anotherUserService = new UserService();
anotherUserService.createUser("Bob");
// Verify singleton scope for logger (should show the same creation timestamp)
const logger1 = Container.get(ILogger);
const logger1Again = Container.get(ILogger);
console.log('Are logger1 and logger1Again the same instance?', logger1 === logger1Again);