This package is a simple dependency injection container for JavaScript and TypeScript. It is inspired by the PHP Pimple package and provides advanced features like wiring and service providers.
- Simple and lightweight - Easy to use dependency injection container
- Auto-wiring - Manual dependency resolution for TypeScript classes with automatic singleton caching
- Singleton behavior - Automatic instance caching ensures classes return the same instance
- Factory support - Create new instances on each request
- Protected callables - Store functions without executing them
- Frozen keys - Prevent modification after first resolution
- Service providers - Modular service registration
- ValueObject management - Isolated dependency management with advanced features
- Circular dependency detection - Built-in prevention of circular dependencies
- Comprehensive error handling - Detailed error classes for better debugging
- Comprehensive testing - Full test coverage with Poku
- Rich examples - Multiple usage patterns and real-world scenarios
# NPM
npm install @davidcromianski-dev/ant-di
# PNPM
pnpm add @davidcromianski-dev/ant-diThis package has zero runtime dependencies, making it lightweight and avoiding dependency conflicts.
The following dependencies are used for development, testing, and building:
- poku - Fast test runner for Node.js
- ts-node - TypeScript execution engine for Node.js
- tsx - TypeScript execution engine with esbuild
- typescript - TypeScript compiler
- vite - Build tool and dev server
- vite-plugin-dts - TypeScript declaration file generation for Vite
This package is fully compatible with Jest and other CommonJS-based testing frameworks. The build process generates both ESM and CommonJS outputs:
- ESM:
dist/index.es.js- For modern bundlers and ES modules - CommonJS:
dist/index.cjs.js- For Jest, Node.js, and CommonJS environments
To use this package with Jest, simply import from the CommonJS build:
// Jest test file
const { Container } = require('@davidcromianski-dev/ant-di');
describe('Container Tests', () => {
it('should create a container instance', () => {
const container = new Container();
expect(container).toBeInstanceOf(Container);
});
});import { Container } from '@davidcromianski-dev/ant-di';
// Create container
const container = new Container();
// Register a service
container.set('database', () => new DatabaseConnection());
// Get the service
const db = container.get('database');
// Auto-wiring example
class UserService {
constructor(private db: DatabaseConnection) {}
}
container.bind(UserService, [DatabaseConnection]);
const userService = container.get(UserService);The container is the main class of the package. It is used to store the services and parameters of the application.
import { Container } from '@davidcromianski-dev/ant-di';
// Empty container
const container = new Container();
// Container with initial values
const container = new Container({
'app.name': 'My Application',
'app.version': '1.0.0',
});To register services in the container, you can use either the set method
(recommended) or offsetSet method:
// Simple value
container.set('app.name', 'My Application');
// Factory function (traditional method)
container.set('database', () => new DatabaseConnection());
// Factory function (direct method)
container.set('database', () => new DatabaseConnection(), true);
// Class constructor
container.set('logger', Logger);
// Legacy method (still supported)
container.offsetSet('app.name', 'My Application');[!NOTE] The
setmethod is the recommended way to register services. TheoffsetSetmethod is maintained for backward compatibility.The third parameter
factory(default: false) allows you to directly register a function as a factory without callingcontainer.factory()first. Whenfactory=true, the function will be executed each time the service is requested.
To get services from the container, you can use the get method:
// Get by string key
const appName = container.get('app.name');
// Get by class constructor (auto-wiring)
const logger = container.get(Logger);The container provides methods for managing its lifecycle:
// Clear all registered services
container.clear();
// Dispose of the container (calls clear internally)
container.dispose();[!TIP] Use
clear()when you want to reset the container to its initial state, anddispose()when you're completely done with the container instance.
[!NOTE] If the service is a factory, it will be executed every time it is requested.
The following methods are the recommended way to interact with the container:
// Service registration
container.set('key', value);
container.set('key', factory, true);
// Service retrieval
container.get('key');
container.get(Constructor);
// Service management
container.has('key');
container.unset('key');The following methods are deprecated but still supported for backward compatibility:
// Deprecated methods (still work, but not recommended)
container.offsetSet('key', value);
container.offsetGet('key');
container.offsetExists('key');
container.offsetUnset('key');[!IMPORTANT] Migration Guide: All deprecated methods now internally call their modern equivalents:
offsetSet()→set()offsetGet()→get()offsetExists()→has()offsetUnset()→unset()Your existing code will continue to work, but consider migrating to the new methods for better maintainability.
[!CAUTION] If the service is not in the container, an exception will be thrown.
To check if a service is registered in the container, you can use the has
method:
const exists = container.has('service');To remove services from the container, you can use the unset method:
container.unset('service');Ant DI supports manual dependency injection for TypeScript classes. You can register dependencies manually.
class UserService {
constructor(
private db: DatabaseConnection,
private logger: Logger,
) {}
}
// Register dependencies manually
container.bind(UserService, [DatabaseConnection, Logger]);
// Get instance with auto-wired dependencies
const userService = container.get(UserService);For cases where you need to bind dependencies using class names as strings (useful for dynamic registration or avoiding circular import issues):
// Alternative binding method using class name
container.bind('UserService', [DatabaseConnection, Logger]);
// Both binding methods achieve the same result
const userService = container.get(UserService);[!TIP] The
bind()method accepts both constructor functions and class name strings, making it flexible for various use cases including dynamic class loading or avoiding potential circular import issues in complex applications.
class ServiceA {
constructor(public serviceB: ServiceB) {}
}
class ServiceB {
constructor(public serviceA: ServiceA) {}
}
// This will throw a circular dependency error
container.bind(ServiceA, [ServiceB]);
container.bind(ServiceB, [ServiceA]);To register factories in the container, you can use multiple methods:
Method 1: Using the factory method (traditional)
const factory = (c: Container) => new Service();
container.factory(factory);
container.set('service', factory);Method 2: Using set with factory parameter (recommended)
const factory = (c: Container) => new Service();
container.set('service', factory, true); // true = register as factory[!TIP] All three methods are equivalent. The
setmethod withfactory=trueis the recommended approach as it provides a more direct way to register factories without needing to callfactory()first.
[!TIP] Useful for services that need to be created every time they are requested.
To protect services in the container, you can use the protect method:
const callable = (c: Container) => new Service();
container.protect(callable);
container.set('service', callable);[!TIP] By default, Ant DI assumes that any callable added to the container is a factory for a service, and it will invoke it when the key is accessed. The
protect()method overrides this behavior, allowing you to store the callable itself.
Keys become frozen after first resolution of implicit factories:
// This creates an implicit factory
container.set('service', (c: Container) => new Service());
// First access - works fine
const service = container.get('service');
// Second access - throws error (key is frozen)
container.set('service', 'new value'); // Error!To get raw values from the container, you can use the raw method:
const rawValue = container.raw('service');[!TIP] Useful when you need access to the underlying value (such as a closure or callable) itself, rather than the result of its execution.
To get all keys registered in the container, you can use the keys method:
const keys = container.keys();Service providers allow you to organize the registration of services in the container.
import { Container, IServiceProvider } from '@davidcromianski-dev/ant-di';
class DatabaseServiceProvider implements IServiceProvider {
register(container: Container) {
container.set('db.host', 'localhost');
container.set('db.port', 5432);
const connectionFactory = (c: Container) => ({
host: c.get('db.host'),
port: c.get('db.port'),
connect: () => `Connected to ${c.get('db.host')}:${c.get('db.port')}`,
});
container.factory(connectionFactory);
container.set('db.connection', connectionFactory);
}
}
// Register the service provider
container.register(new DatabaseServiceProvider());
// Use the services
const connection = container.get('db.connection');
console.log(connection.connect());Ant DI automatically caches class instances to ensure singleton behavior:
class DatabaseService {
constructor() {
console.log('DatabaseService created');
}
}
// Register the class
container.bind(DatabaseService, []);
// First access - creates instance
const db1 = container.get(DatabaseService); // "DatabaseService created"
// Second access - returns cached instance
const db2 = container.get(DatabaseService); // No log (cached)
console.log(db1 === db2); // true - same instance[!TIP] Class instances are cached by their constructor function, ensuring that the same class always returns the same instance across the application.
Ant DI provides advanced ValueObject management for isolated dependency handling:
import { Container, ValueObject } from '@davidcromianski-dev/ant-di';
class DatabaseService {
constructor(public config: any) {}
}
class UserService {
constructor(public db: DatabaseService) {}
}
const container = new Container();
// Create ValueObject with dependencies
const dbValueObject = new ValueObject(
container,
'DatabaseService',
DatabaseService,
);
dbValueObject.addDependency('dbConfig');
// Register configuration
container.set('dbConfig', { host: 'localhost', port: 5432 });
// Register service with ValueObject
container.set('DatabaseService', DatabaseService, false, ['dbConfig']);
// Get ValueObject from container
const valueObject = container.getValueObject('DatabaseService');
// ValueObject management methods
console.log('Has dependency:', valueObject?.hasDependency('dbConfig'));
console.log('Dependencies:', valueObject?.getDependencyKeys());
console.log('Is resolvable:', valueObject?.isResolvable());
// Create instance with dependency injection
const dbInstance = valueObject?.getValue();
console.log('Database instance:', dbInstance instanceof DatabaseService);- Dependency Management: Add, remove, and validate dependencies
- Circular Dependency Detection: Automatic detection and prevention
- Instance Creation: Singleton and factory patterns
- Cloning: Create copies of ValueObjects
- Validation: Check if dependencies are resolvable
// Advanced ValueObject usage
const valueObject = new ValueObject(container, 'UserService', UserService);
// Add multiple dependencies
valueObject
.addDependency('DatabaseService')
.addDependency('Logger')
.asSingleton();
// Validate dependencies
if (valueObject.isResolvable()) {
const instance = valueObject.getValue();
console.log('Service created:', instance);
}
// Clone ValueObject
const cloned = valueObject.clone();
console.log('Cloned dependencies:', cloned.getDependencyKeys());
// Clear all dependencies
valueObject.clearDependencies();
console.log(
'Dependencies cleared:',
valueObject.getDependencyKeys().length === 0,
);The container supports proxy access for convenient property-style access:
// Set value
container.appName = 'My App';
// Get value
console.log(container.appName); // "My App"The project uses Poku for testing. All tests
are located in the tests/ directory.
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:coverageTests are organized into logical groups:
- Basic Operations - Core container functionality
- Factory Operations - Factory and protection features
- Auto-wiring - Dependency injection and resolution
- Frozen Keys - Key freezing behavior
- Proxy Access - Proxy functionality
- Constructor Initialization - Container initialization
- Service Providers - Service provider registration
import { describe, it, assert } from 'poku';
import { Container } from '../src';
describe('Container', () => {
describe('Basic Operations', () => {
it('should set and get a value', () => {
const container = new Container();
container.set('key', 'value');
const value = container.get('key');
assert.equal(value, 'value');
});
});
});Comprehensive examples are available in the examples/ directory:
npx ts-node examples/basic-usage.tsDemonstrates simple value storage, factory functions, and basic container operations.
npx ts-node examples/dependency-injection.tsShows auto-wiring, manual dependency registration, and circular dependency detection.
npx ts-node examples/factories-and-protection.tsExplains factory functions, protected callables, and frozen key behavior.
npx ts-node examples/service-providers.tsDemonstrates modular service registration using service providers.
npx ts-node examples/advanced-patterns.tsReal-world scenarios including event systems and complex dependency graphs.
npx ts-node examples/index.tsnew Container(values?: Record<string, ValueOrFactoryOrCallable<any>>)set<T>(key: string, value: ValueOrFactoryOrCallable<T>, factory?: boolean): void- Recommended method to register a value, factory, or callable in the container. The optionalfactoryparameter (default: false) allows direct factory registration when set to true.get<T>(key: string | Constructor<T>): T- Recommended method to retrieve a service by key or class constructor with auto-wiringhas(key: string): boolean- Recommended method to check if a key exists in the containerunset(key: string): void- Recommended method to remove a key from the container
offsetSet<T>(key: string, value: ValueOrFactoryOrCallable<T>, factory?: boolean): void- Deprecated. Useset()instead.offsetGet<T>(key: string | Constructor<T>): T- Deprecated. Useget()instead.offsetExists(key: string): boolean- Deprecated. Usehas()instead.offsetUnset(key: string): void- Deprecated. Useunset()instead.
factory<T>(factory: Factory<T>): Factory<T>- Mark a factory to always create new instances (prevents singleton caching)protect(callable: Callable): Callable- Protect a callable from being treated as a factory
raw<T>(key: string): T- Get the raw value without executing factories or callableskeys(): string[]- Get all registered keys in the containerclear(): void- Clear all registered services and reset the container to its initial statedispose(): void- Dispose of the container and clean up resources
register(provider: IServiceProvider, values?: Record<string, ValueOrFactoryOrCallable<any>>): Container- Register a service provider with optional additional values
bind(target: Function | string, dependencies: any[]): void- Bind dependencies for a class constructor or class name stringgetValueObject(key: string): ValueObjectInterface | undefined- Get a ValueObject for a given key if it exists
addDependency(dependency: DependencyKey): this- Add a dependency to the ValueObjectremoveDependency(dependency: DependencyKey): this- Remove a specific dependencyhasDependency(dependency: DependencyKey): boolean- Check if a dependency existsvalidateDependencies(): boolean- Validate all dependencies are availableresolveDependencies(): any[]- Resolve all dependencies with circular dependency detectionisResolvable(): boolean- Check if the ValueObject can be resolvedgetDependencyKeys(): DependencyKey[]- Get all dependency keysclearDependencies(): this- Clear all dependenciesclone(): ValueObjectInterface- Create a copy of the ValueObject
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests for new functionality
- Run the test suite
- Submit a pull request
This project is licensed under the MIT License. For more details, see the LICENSE file.