Container
The Container
class is the core of Vercube's dependency injection system. It manages the registration and resolution of dependencies, handling their lifecycle and ensuring proper instantiation.
Class Definition
class Container {
constructor(options?: ContainerOptions);
// Registration methods
bind<T>(key: ServiceKey<T>, value?: ServiceValue<T>): void;
bindTransient<T>(key: ServiceKey<T>, value?: ServiceValue<T>): void;
bindInstance<T>(key: ServiceKey<T>, instance: T): void;
bindMock<T>(key: ServiceKey<T>, mock: T): void;
// Resolution methods
get<T>(key: ServiceKey<T>): T;
getOptional<T>(key: ServiceKey<T>): T | undefined;
resolve<T>(target: Constructor<T>): T;
// Utility methods
has(key: ServiceKey): boolean;
clear(): void;
}
Registration Methods
bind<T>(key: ServiceKey<T>, value?: ServiceValue<T>): void
Registers a service as a singleton. If no value is provided, the key itself is used as the implementation.
// Register a class
container.bind(Logger, ConsoleLogger);
// Register an interface with implementation
container.bind(ILogger, ConsoleLogger);
// Register a class as its own implementation
container.bind(UserService);
bindTransient<T>(key: ServiceKey<T>, value?: ServiceValue<T>): void
Registers a service as transient, creating a new instance each time it's resolved.
// Register a transient service
container.bindTransient(Logger, ConsoleLogger);
// Register a class as transient
container.bindTransient(UserService);
bindInstance<T>(key: ServiceKey<T>, instance: T): void
Registers an existing instance as a singleton.
const logger = new ConsoleLogger();
container.bindInstance(Logger, logger);
bindMock<T>(key: ServiceKey<T>, mock: T): void
Registers a mock implementation, useful for testing.
const mockLogger = {
info: jest.fn(),
error: jest.fn()
};
container.bindMock(Logger, mockLogger);
Service Overriding
Services can be overridden by registering a new implementation for the same key. This is useful for:
- Replacing implementations in different environments
- Swapping implementations for testing
- Upgrading services without changing dependent code
Overriding a Service
// Register the default implementation
container.bind(Logger, ConsoleLogger);
// Later, override with a different implementation
container.bind(Logger, FileLogger);
Overriding in Child Containers
Child containers can override services from their parent container:
// Parent container
const parentContainer = new Container();
parentContainer.bind(Logger, ConsoleLogger);
// Child container overrides the Logger
const childContainer = new Container(parentContainer);
childContainer.bind(Logger, FileLogger);
// This will use FileLogger
const logger = childContainer.get(Logger);
Conditional Overriding
You can conditionally override services based on configuration:
function setupContainer(container: Container, config: Config) {
// Register base services
container.bind(UserService);
container.bind(ProductService);
// Override based on environment
if (config.env === 'production') {
container.bind(Logger, ProductionLogger);
container.bind(Database, ProductionDatabase);
} else if (config.env === 'testing') {
container.bind(Logger, TestLogger);
container.bind(Database, InMemoryDatabase);
} else {
container.bind(Logger, ConsoleLogger);
container.bind(Database, SQLiteDatabase);
}
}
Overriding for Testing
Service overriding is particularly useful for testing:
describe('UserService', () => {
let container: Container;
let userService: UserService;
beforeEach(() => {
container = new Container();
// Register real implementations
container.bind(Logger, ConsoleLogger);
container.bind(Database, SQLiteDatabase);
// Override with mocks for testing
container.bindMock(UserRepository, {
findById: jest.fn().mockResolvedValue({ id: '123', name: 'Test User' })
});
userService = container.resolve(UserService);
});
it('should find user by id', async () => {
const user = await userService.getUser('123');
expect(user).toBeDefined();
});
});
Resolution Methods
get<T>(key: ServiceKey<T>): T
Resolves a service instance. Throws an error if the service is not registered.
const logger = container.get(Logger);
logger.info('Hello, world!');
getOptional<T>(key: ServiceKey<T>): T | undefined
Resolves a service instance. Returns undefined if the service is not registered.
const logger = container.getOptional(Logger);
if (logger) {
logger.info('Logger is available');
}
resolve<T>(target: Constructor<T>): T
Creates a new instance of a class, automatically resolving its dependencies.
class UserController {
@Inject(UserService)
private userService: UserService,
@Inject(Logger)
private logger: Logger
}
const controller = container.resolve(UserController);
Utility Methods
has(key: ServiceKey): boolean
Checks if a service is registered in the container.
if (container.has(Logger)) {
console.log('Logger is registered');
}
clear(): void
Clears all registered services from the container.
container.clear();
Examples
Basic Usage
// Create a container
const container = new Container();
// Register services
container.bind(Logger, ConsoleLogger);
container.bind(UserService);
container.bind(UserController);
// Resolve and use a service
const controller = container.resolve(UserController);
await controller.getUser('123');
Testing Setup
// Create a container for testing
const container = new Container();
// Register mock services
container.bindMock(Logger, {
info: jest.fn(),
error: jest.fn()
});
container.bindMock(UserService, {
getUser: jest.fn().mockResolvedValue({ id: '123', name: 'Test User' })
});
// Resolve the controller with mock dependencies
const controller = container.resolve(UserController);
Scoped Services
// Register a singleton service
container.bind(ConfigService);
// Register a transient service
container.bindTransient(Logger, ConsoleLogger);
// Register an instance
const db = new Database();
container.bindInstance(Database, db);
// Use the services
class App {
@Inject(ConfigService)
private config!: ConfigService;
@Inject(Logger)
private logger!: Logger;
@Inject(Database)
private db!: Database;
}
Best Practices
Register Services Early
- Register all services during application bootstrap
- Use a dedicated configuration file for service registration
Use Appropriate Scopes
- Use singletons for stateless services
- Use transient for services that need fresh instances
- Use instances for external resources
Handle Errors Gracefully
- Use
getOptional
when a service might not be available - Provide fallback implementations when needed
- Use
Clean Up Resources
- Use
clear()
when shutting down the application - Implement proper cleanup in services
- Use
Use Service Overriding Strategically
- Override services for different environments
- Use overriding for testing
- Document when services are overridden
See Also
- Decorators - Available decorators for dependency injection
- Types - Type definitions used in the DI system
- Advanced Topics - Advanced usage patterns and techniques