TypeScript Dependency Injection Container
ts-ioc-container is a fast, lightweight TypeScript dependency injection
container for applications that need more than basic constructor injection. It
keeps the API clear while supporting scoped lifecycles, decorators, typed tokens,
lazy dependencies, lifecycle hooks, provider pipelines, and custom injector
strategies.
Use it when you want tsyringe-style speed and simplicity with richer controls, or a thinner and cleaner API than heavier containers such as Inversify and Awilix.
Why Use ts-ioc-container?
- Fast TypeScript dependency resolution with benchmark coverage against
tsyringe - Clean API for registering and resolving services by class, key, token, or alias
- No global container object; pass scopes explicitly through your app boundary
- Tagged scopes for application, request, transaction, page, and widget lifecycles
- Decorator support with
@register,@inject,@onConstruct, and@onDispose - Lazy dependencies, property injection, custom hooks, and provider decoration
- Composable provider and registration pipes for project-specific behavior
tsyringe Alternative
ts-ioc-container is a practical tsyringe alternative when you want the same
kind of lightweight TypeScript DI experience, but also need request scopes,
custom providers, explicit tokens, grouped aliases, lifecycle cleanup, and
pluggable injector strategies.
Read the tsyringe alternative comparison for the full positioning.
Inversify and Awilix Alternative
ts-ioc-container is intentionally thinner than Inversify-style enterprise DI
and cleaner than container APIs that push applications toward broad global
objects. You create containers and scopes explicitly, pass them through the
application boundary, and compose behavior through providers, tokens, hooks, and
pipes.
Read the Inversify and Awilix alternative comparison for the clean API and no-global-container positioning.
Quick Start
npm install ts-ioc-container reflect-metadatanpm install ts-ioc-container reflect-metadatayarn add ts-ioc-container reflect-metadatayarn add ts-ioc-container reflect-metadataPut this in your entrypoint file (should be the first line):
import 'reflect-metadata';import 'reflect-metadata';Configure tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}Basic Example: User Authentication
This example demonstrates a real-world pattern: an AuthService that depends on a UserRepository for database access. The container automatically wires up the dependencies.
import 'reflect-metadata';
import { bindTo, Container, type IContainer, inject, register, Registration as R, select } from 'ts-ioc-container';
/**
* User Management Domain - Basic Dependency Injection
*
* This example demonstrates how to wire up a simple authentication service
* that depends on a user repository. This pattern is common in web applications
* where services need database access.
*/
describe('Basic usage', function () {
// Domain types
interface User {
id: string;
email: string;
passwordHash: string;
}
// Repository interface - abstracts database access
interface IUserRepository {
findByEmail(email: string): User | undefined;
}
// Concrete implementation
@register(bindTo('IUserRepository'))
class UserRepository implements IUserRepository {
private users: User[] = [{ id: '1', email: 'admin@example.com', passwordHash: 'hashed_password' }];
findByEmail(email: string): User | undefined {
return this.users.find((u) => u.email === email);
}
}
it('should inject dependencies', function () {
// AuthService depends on IUserRepository
class AuthService {
constructor(@inject('IUserRepository') private userRepo: IUserRepository) {}
authenticate(email: string): boolean {
const user = this.userRepo.findByEmail(email);
return user !== undefined;
}
}
// Wire up the container
const container = new Container().addRegistration(R.fromClass(UserRepository));
// Resolve AuthService - UserRepository is automatically injected
const authService = container.resolve(AuthService);
expect(authService.authenticate('admin@example.com')).toBe(true);
expect(authService.authenticate('unknown@example.com')).toBe(false);
});
it('should inject current scope for request context', function () {
// In Express.js, each request gets its own scope
// Services can access the current scope to resolve request-specific dependencies
const appContainer = new Container({ tags: ['application'] });
class RequestHandler {
constructor(@inject(select.scope.current) public requestScope: IContainer) {}
handleRequest(): string {
// Access request-scoped dependencies
return this.requestScope.hasTag('application') ? 'app-scope' : 'request-scope';
}
}
const handler = appContainer.resolve(RequestHandler);
expect(handler.requestScope).toBe(appContainer);
expect(handler.handleRequest()).toBe('app-scope');
});
});
import 'reflect-metadata';
import { bindTo, Container, type IContainer, inject, register, Registration as R, select } from 'ts-ioc-container';
/**
* User Management Domain - Basic Dependency Injection
*
* This example demonstrates how to wire up a simple authentication service
* that depends on a user repository. This pattern is common in web applications
* where services need database access.
*/
describe('Basic usage', function () {
// Domain types
interface User {
id: string;
email: string;
passwordHash: string;
}
// Repository interface - abstracts database access
interface IUserRepository {
findByEmail(email: string): User | undefined;
}
// Concrete implementation
@register(bindTo('IUserRepository'))
class UserRepository implements IUserRepository {
private users: User[] = [{ id: '1', email: 'admin@example.com', passwordHash: 'hashed_password' }];
findByEmail(email: string): User | undefined {
return this.users.find((u) => u.email === email);
}
}
it('should inject dependencies', function () {
// AuthService depends on IUserRepository
class AuthService {
constructor(@inject('IUserRepository') private userRepo: IUserRepository) {}
authenticate(email: string): boolean {
const user = this.userRepo.findByEmail(email);
return user !== undefined;
}
}
// Wire up the container
const container = new Container().addRegistration(R.fromClass(UserRepository));
// Resolve AuthService - UserRepository is automatically injected
const authService = container.resolve(AuthService);
expect(authService.authenticate('admin@example.com')).toBe(true);
expect(authService.authenticate('unknown@example.com')).toBe(false);
});
it('should inject current scope for request context', function () {
// In Express.js, each request gets its own scope
// Services can access the current scope to resolve request-specific dependencies
const appContainer = new Container({ tags: ['application'] });
class RequestHandler {
constructor(@inject(select.scope.current) public requestScope: IContainer) {}
handleRequest(): string {
// Access request-scoped dependencies
return this.requestScope.hasTag('application') ? 'app-scope' : 'request-scope';
}
}
const handler = appContainer.resolve(RequestHandler);
expect(handler.requestScope).toBe(appContainer);
expect(handler.handleRequest()).toBe('app-scope');
});
});
Express.js Integration
Here’s how to integrate the container with Express.js for request-scoped dependencies:
import express from 'express';
import { Container, type IContainer } from 'ts-ioc-container';
// Extend Express Request type
declare global {
namespace Express {
interface Request {
container: IContainer;
}
}
}
// Application container (singleton services)
const appContainer = new Container({ tags: ['application'] });
// ... register your services
// Middleware: Create request scope
app.use((req, res, next) => {
req.container = appContainer.createScope({ tags: ['request'] });
res.on('finish', () => req.container.dispose());
next();
});
// Route handler
app.post('/login', (req, res) => {
const authService = req.container.resolve<AuthService>('IAuthService');
const result = authService.authenticate(req.body.email, req.body.password);
res.json({ success: result });
});import express from 'express';
import { Container, type IContainer } from 'ts-ioc-container';
// Extend Express Request type
declare global {
namespace Express {
interface Request {
container: IContainer;
}
}
}
// Application container (singleton services)
const appContainer = new Container({ tags: ['application'] });
// ... register your services
// Middleware: Create request scope
app.use((req, res, next) => {
req.container = appContainer.createScope({ tags: ['request'] });
res.on('finish', () => req.container.dispose());
next();
});
// Route handler
app.post('/login', (req, res) => {
const authService = req.container.resolve<AuthService>('IAuthService');
const result = authService.authenticate(req.body.email, req.body.password);
res.json({ success: result });
});Documentation
Browse the documentation chapters in the navigation menu to learn about:
- Overview - Core architecture and class diagrams
- Product Capabilities - Product capability map from user outcomes to specs and docs
- tsyringe Alternative - Feature comparison for teams evaluating TypeScript DI libraries
- Inversify and Awilix Alternative - Cleaner API, thinner runtime model, and no global container object
- Dependency and Scope - Dependency resolution, scoped lifecycle, instance tracking, and disposal
- Dependency Registration - Classes, values, factories, keys, aliases, scope rules, and modules
- Provider Behavior - Dependency factories with singleton, args, visibility, decoration, and lazy behavior
- Pipes - Provider and registration behavior composition
- Token-Based Injection - Explicit token shapes for keys, aliases, classes, functions, constants, and instances
- Injector Strategies - Metadata, Simple, Proxy, and custom construction strategies
- Lifecycle Hooks - Initialization, cleanup, property injection, custom hooks, and hook execution rules
- Metadata Utilities - Class, parameter, method, label, tag, and method behavior decorators
- Errors and Boundaries - Missing dependencies, invalid registrations, disposed scopes, unsupported tokens, hooks, and EmptyContainer behavior
Core Architecture
This chapter provides an overview of the core architecture and design of the TypeScript IoC container library. Understanding these concepts will help you make the most of the library’s features.
The IoC container is built on four fundamental abstractions that work together to provide dependency injection:
Container
Manages the lifecycle and resolution of dependencies
Provider
Factory that creates dependency instances
Injector
Determines how dependencies are injected into constructors
Registration
Connects providers to containers with keys and configuration
Architecture Overview
Class Diagram
The following diagram shows the main classes and interfaces and their relationships: