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.

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.

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.

Bash
npm install ts-ioc-container reflect-metadata
npm install ts-ioc-container reflect-metadata
Bash
yarn add ts-ioc-container reflect-metadata
yarn add ts-ioc-container reflect-metadata

Put this in your entrypoint file (should be the first line):

TypeScript
import 'reflect-metadata';
import 'reflect-metadata';

Configure tsconfig.json:

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.

TypeScript __tests__/readme/basic.spec.ts
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:

TypeScript Express.js integration
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 });
});

Browse the documentation chapters in the navigation menu to learn about:

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

graph TB Container[Container] Provider[IProvider] Injector[IInjector] Registration[IRegistration] Scope[Container Scope] Container -->|uses| Provider Container -->|uses| Injector Container -->|creates| Scope Registration -->|creates| Provider Container -->|manages| Registration Provider -->|resolves| Instance[Instance] Injector -->|injects| Instance style Container fill:#0366d6,color:#fff style Provider fill:#28a745,color:#fff style Injector fill:#ffc107,color:#000 style Registration fill:#17a2b8,color:#fff style Scope fill:#6f42c1,color:#fff style Instance fill:#dc3545,color:#fff

The following diagram shows the main classes and interfaces and their relationships:

classDiagram class IContainer { +resolve(key) T +createScope(options) IContainer +register(key, provider) this +addRegistration(registration) this +dispose() void +getInstances() Instance[] +hasTag(tag) boolean } class Container { -parent: IContainer -scopes: IContainer[] -instances: Instance[] -providers: Map -injector: IInjector -tags: Set +resolve(key) T +createScope(options) IContainer +register(key, provider) this +dispose() void } class IProvider { +resolve(container, options) T +hasAccess(options) boolean +pipe(mappers) IProvider +setAccessRule(rule) this +appendArgs(args) this +appendArgsFn(fn) this +lazy() this } class Provider { -factory: ResolveDependency -argsFn: ArgsFn -accessRule: ScopeAccessRule -isLazy: boolean +resolve(container, options) T +pipe(mappers) IProvider } class SingletonProvider { -cache: Cache +resolve(container, options) T } class IInjector { +resolve(container, constructor, options) T } class MetadataInjector { +resolve(container, constructor, options) T -createInstance(container, constructor, options) T } class SimpleInjector { +resolve(container, constructor, options) T -createInstance(container, constructor, options) T } class ProxyInjector { +resolve(container, constructor, options) T -createInstance(container, constructor, options) T } class IRegistration { +when(predicates) this +bindToKey(key) this +bindTo(key) this +pipe(mappers) this +applyTo(container) void } class Registration { -provider: IProvider -key: DependencyKey -scopeRules: ScopeMatchRule[] +when(predicates) this +bindToKey(key) this +applyTo(container) void } IContainer <|.. Container IContainer <|.. EmptyContainer Container --> IProvider : uses Container --> IInjector : uses Container --> IRegistration : manages IProvider <|.. Provider IProvider <|.. SingletonProvider IInjector <|.. MetadataInjector IInjector <|.. SimpleInjector IInjector <|.. ProxyInjector IRegistration <|.. Registration Registration --> IProvider : creates Container --> Container : parent-child