iocbox
v1.0.1
Published
Type-safe, zero-dependency DI container for TypeScript with lifetime management
Maintainers
Readme
iocbox
A type-safe, zero-dependency Dependency Injection container for TypeScript with lifetime management and async disposal support.
Features
- Zero runtime dependencies - Pure TypeScript implementation
- Type-safe - Full compile-time type checking with Symbol-based keys
- Lifetime management - Singleton, Scoped, and Transient lifetimes
- Async disposal - Built-in
Symbol.asyncDisposesupport for resource cleanup - Module system - Organize registrations into reusable modules
- Dual format - Works with both ESM and CommonJS
Installation
npm install iocboxQuick Start
import { ContainerBuilder, key, LifeTimeScope } from "iocbox";
// Define service keys with types
const LoggerKey = key<Logger>("Logger");
const DatabaseKey = key<Database>("Database");
const UserServiceKey = key<UserService>("UserService");
// Create container with builder
const builder = new ContainerBuilder();
builder.register(LoggerKey, () => new ConsoleLogger(), LifeTimeScope.Singleton);
builder.register(DatabaseKey, () => new PostgresDatabase(), LifeTimeScope.Singleton);
builder.register(
UserServiceKey,
(container) => new UserService(
container.resolve(LoggerKey),
container.resolve(DatabaseKey)
),
LifeTimeScope.Scope
);
const container = builder.build();
// Resolve services
const userService = container.resolve(UserServiceKey);Lifetime Scopes
Transient
Creates a new instance on every resolve() call.
builder.register(ServiceKey, () => new Service(), LifeTimeScope.Transient);Singleton
Single instance per root container lifetime. Cached at root level.
builder.register(ServiceKey, () => new Service(), LifeTimeScope.Singleton);Scope (Default)
Single instance per container scope. Fresh instance per createScope().
builder.register(ServiceKey, () => new Service(), LifeTimeScope.Scope);
// or simply (Scope is the default):
builder.register(ServiceKey, () => new Service());Scoped Containers
Create child scopes for request-specific isolation:
const container = builder.build();
async function handleRequest(request: Request) {
await using scope = container.createScope(Symbol("request"));
const service = scope.resolve(RequestServiceKey);
return service.handle(request);
// Scope automatically disposed after this block
}Modules
Organize registrations into reusable modules:
import { Builder, Module, LifeTimeScope } from "iocbox";
const databaseModule: Module = (builder: Builder) => {
builder.register(PoolKey, () => new Pool(), LifeTimeScope.Singleton);
builder.register(
RepositoryKey,
(c) => new Repository(c.resolve(PoolKey)),
LifeTimeScope.Scope
);
};
const builder = new ContainerBuilder();
builder.registerModule(databaseModule);
const container = builder.build();Async Disposal
Services implementing Disposable or AsyncDisposable are automatically cleaned up:
import { BaseDisposable, key, ContainerBuilder, LifeTimeScope } from "iocbox";
class DatabaseConnection extends BaseDisposable {
protected async onDispose(): Promise<void> {
await this.connection.close();
}
}
const ConnectionKey = key<DatabaseConnection>("Connection");
builder.register(ConnectionKey, () => new DatabaseConnection(), LifeTimeScope.Singleton);
// Later...
await container[Symbol.asyncDispose](); // Closes all connectionsAPI Reference
key<T>(name: string): Key<T>
Creates a type-safe service key.
ContainerBuilder
| Method | Description |
|--------|-------------|
| register(key, factory, scope?) | Register a service factory |
| registerModule(module) | Register a module |
| build() | Build the container (one-time) |
Resolve (Container interface)
| Method | Description |
|--------|-------------|
| resolve<T>(key) | Resolve a service by key |
| createScope(name) | Create a child scope |
| [Symbol.asyncDispose]() | Dispose container and services |
LifeTimeScope
| Value | Description |
|-------|-------------|
| Transient (0) | New instance per resolve |
| Singleton (1) | Single instance per root |
| Scope (2) | Single instance per scope |
BaseDisposable
Abstract base class for services requiring cleanup. Implement onDispose() for your cleanup logic.
Type Guards
isDisposable(value)- Check forSymbol.disposeisAsyncDisposable(value)- Check forSymbol.asyncDispose
Error Handling
The container throws descriptive errors:
- Key not registered:
Cannot resolve object. There is no registry with key "Symbol(name)" - Scoped from singleton:
Cannot resolve object. Scoped object 'Symbol(name)' cannot be resolved from Singleton - Duplicate registration:
Duplicate registration "Symbol(name)" - Build twice:
Container has been already built
Debugging
Enable console logging for DI resolution:
import { Container } from "iocbox";
Container.logToConsole = true;License
MIT
