A lightweight, TypeScript-first dependency injection container with support for both synchronous and asynchronous service resolution.
- 🚀 Lightweight: Minimal overhead with no external dependencies (except reflect-metadata for decorators)
- 🔄 Sync & Async: Support for both synchronous and asynchronous service factories
- 🛡️ Type Safe: Full TypeScript support with comprehensive type definitions
- 🔁 Lifetime Management: Singleton and transient service lifetimes
- 🎯 Flexible Tokens: Register services using classes, strings, or symbols
- 🚫 Circular Dependency Detection: Automatic detection and prevention of circular dependencies
- 🎨 Decorator Support: Optional decorator-based dependency injection
- 🧹 Automatic Cleanup: Built-in disposal pattern for resource cleanup
- 📊 Logging: Optional logging for debugging and monitoring
- ⚡ Performance Monitoring: Built-in performance tracking and metrics collection
pnpm add @lexms/di-container
For decorator support, also install:
pnpm add reflect-metadata
import { DIContainer, LifetimeScope } from '@lexms/di-container';
// Create a container
const container = new DIContainer();
// Define services
class DatabaseService {
connect() {
console.log('Connected to database');
}
}
class UserService {
constructor(private db: DatabaseService) {}
getUsers() {
this.db.connect();
return ['user1', 'user2'];
}
}
// Register services
container.registerSingleton(DatabaseService, () => new DatabaseService());
container.registerSingleton(UserService, () =>
new UserService(container.resolve(DatabaseService))
);
// Resolve and use
const userService = container.resolve(UserService);
const users = userService.getUsers();
const container = new DIContainer(options?: DIContainerOptions);
Options:
enableLogging?: boolean
- Enable debug logging (default: false)logPrefix?: string
- Custom log prefix (default: 'DIContainer')enablePerformanceMonitoring?: boolean
- Enable performance tracking (default: false)
Register a service with explicit lifetime scope.
container.register(MyService, () => new MyService(), LifetimeScope.SINGLETON);
Register a singleton service (same instance returned on every resolve).
container.registerSingleton(MyService, () => new MyService());
Register a transient service (new instance returned on every resolve).
container.registerTransient(MyService, () => new MyService());
Register an existing instance.
const config = { apiUrl: 'https://api.example.com' };
container.registerInstance('config', config);
Synchronously resolve a service.
const service = container.resolve(MyService);
Asynchronously resolve a service (required for async factories).
const service = await container.resolveAsync(MyAsyncService);
Check if a service is registered.
if (container.has(MyService)) {
// Service is registered
}
Get all registered service tokens.
const tokens = container.getRegisteredTokens();
Remove all service registrations.
container.clear();
Dispose the container and call dispose()
on all disposable services.
await container.dispose();
Get comprehensive performance statistics for the container.
const stats = container.getPerformanceStats();
console.log(`Total resolutions: ${stats.totalResolutions}`);
console.log(`Average time: ${stats.averageResolutionTime}ms`);
Get performance metrics for specific services or all services.
// Get metrics for all services
const allMetrics = container.getServiceMetrics();
// Get metrics for specific service
const serviceMetrics = container.getServiceMetrics(MyService);
Reset all performance statistics.
container.resetPerformanceStats();
class AsyncDatabaseService {
async connect() {
// Async connection logic
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('Connected to database');
}
}
// Register with async factory
container.registerSingleton(AsyncDatabaseService, async () => {
const service = new AsyncDatabaseService();
await service.connect();
return service;
});
// Must use resolveAsync for async factories
const dbService = await container.resolveAsync(AsyncDatabaseService);
// String tokens
container.registerInstance('apiUrl', 'https://api.example.com');
const apiUrl = container.resolve<string>('apiUrl');
// Symbol tokens
const DATABASE_CONFIG = Symbol('DatabaseConfig');
container.registerInstance(DATABASE_CONFIG, { host: 'localhost', port: 5432 });
const dbConfig = container.resolve<{ host: string; port: number }>(DATABASE_CONFIG);
Enable performance monitoring to track service resolution times and identify bottlenecks.
const container = new DIContainer({
enablePerformanceMonitoring: true,
enableLogging: true
});
class FastService {
process() { return 'fast'; }
}
class SlowService {
process() {
// Simulate slow operation
const start = Date.now();
while (Date.now() - start < 100) {}
return 'slow';
}
}
container.registerSingleton(FastService, () => new FastService());
container.registerTransient(SlowService, () => new SlowService());
// Resolve services multiple times
container.resolve(FastService);
container.resolve(FastService); // Cached singleton
container.resolve(SlowService);
container.resolve(SlowService); // New instance
// Get performance statistics
const stats = container.getPerformanceStats();
console.log(`Total resolutions: ${stats.totalResolutions}`);
console.log(`Average resolution time: ${stats.averageResolutionTime}ms`);
console.log(`Slowest services:`, stats.slowestServices);
// Get service-specific metrics
const metrics = container.getServiceMetrics();
metrics.forEach(metric => {
console.log(`${metric.token}: ${metric.averageTime}ms avg (${metric.totalResolutions} calls)`);
});
// Reset statistics
container.resetPerformanceStats();
Services that implement a dispose()
method will be automatically disposed when the container is disposed.
class ResourceService {
private connection: any;
constructor() {
this.connection = createConnection();
}
async dispose() {
await this.connection.close();
console.log('Resources cleaned up');
}
}
container.registerSingleton(ResourceService, () => new ResourceService());
// Later...
await container.dispose(); // Automatically calls ResourceService.dispose()
First, import reflect-metadata at the top of your main file:
import 'reflect-metadata';
import { Injectable, Inject, autoRegister } from '@lexms/di-container';
@Injectable
class DatabaseService {
connect() {
console.log('Connected to database');
}
}
@Injectable
class UserService {
constructor(
private db: DatabaseService,
@Inject('config') private config: any
) {}
}
// Auto-register decorated classes
autoRegister(DatabaseService);
autoRegister(UserService);
The container provides specific error types for different failure scenarios:
import {
ServiceNotFoundError,
CircularDependencyError,
DIContainerError
} from '@lexms/di-container';
try {
const service = container.resolve(UnregisteredService);
} catch (error) {
if (error instanceof ServiceNotFoundError) {
console.log('Service not found');
} else if (error instanceof CircularDependencyError) {
console.log('Circular dependency detected');
}
}
interface IUserRepository {
getUsers(): User[];
}
class DatabaseUserRepository implements IUserRepository {
getUsers(): User[] {
// Database implementation
return [];
}
}
class MockUserRepository implements IUserRepository {
getUsers(): User[] {
// Mock implementation
return [{ id: 1, name: 'Test User' }];
}
}
// Register based on environment
const repository = process.env.NODE_ENV === 'test'
? new MockUserRepository()
: new DatabaseUserRepository();
container.registerInstance('IUserRepository', repository);
container.registerSingleton(UserService, () => {
const repository = container.resolve<IUserRepository>('IUserRepository');
const logger = container.resolve<ILogger>('ILogger');
const config = container.resolve<Config>('config');
return new UserService(repository, logger, config);
});
class DatabaseModule {
static register(container: DIContainer) {
container.registerSingleton(DatabaseService, () => new DatabaseService());
container.registerSingleton(UserRepository, () =>
new UserRepository(container.resolve(DatabaseService))
);
}
}
class ServiceModule {
static register(container: DIContainer) {
container.registerSingleton(UserService, () =>
new UserService(container.resolve(UserRepository))
);
}
}
// Register all modules
DatabaseModule.register(container);
ServiceModule.register(container);
# Install dependencies
pnpm install
# Build the project
pnpm run build
# Run tests
pnpm test
# Run tests in watch mode
pnpm run test:watch
# Run linting
pnpm run lint
# Fix linting issues
pnpm run lint:fix
# Clean build outputs
pnpm run clean
MIT
Contributions are welcome! Please feel free to submit a Pull Request.