plain-ioc
v1.2.0
Published
Plain inversion of control container
Maintainers
Readme
plain-ioc
A small, dependency-free Inversion of Control (IoC) / Dependency Injection container for Node.js and TypeScript.
Highlights
- Synchronous factories only (factories cannot return a
Promise) - Transient and singleton bindings
- Optional circular dependency detection (useful in development)
- Tiny API surface
Installation
npm install plain-iocCore concepts
Dependency keys
A dependency key is used to register and later resolve a dependency.
In TypeScript, the key type is:
type DependencyKey = string | symbol | object;Recommended key styles:
symbolkeys for interfaces or abstract concepts (prevents accidental collisions)- class constructors as keys for concrete classes
stringkeys for quick scripts or small apps
Tip: Using
Symbol('Name')makes debugging easier.
Factories
A factory creates the dependency instance.
interface DependencyFactory<T> {
(container: Container): T; // must not return a Promise
}Factories receive the Container, so they can resolve other dependencies.
Usage
Create a container
import { Container } from 'plain-ioc';
const container = new Container();Bind and resolve (transient)
bind() registers a factory that runs every time you resolve the key.
import { Container } from 'plain-ioc';
const c = new Container();
c.bind('now', () => Date.now());
const a = c.resolve<number>('now');
const b = c.resolve<number>('now');
// a !== b (very likely)Bind and resolve (singleton)
bindSingleton() registers a factory that runs once. The created instance is cached and returned for subsequent resolves.
import { Container } from 'plain-ioc';
class Logger {
public log(message: string): void {
console.log(message);
}
}
const c = new Container();
c.bindSingleton(Logger, () => new Logger());
const l1 = c.resolve<Logger>(Logger);
const l2 = c.resolve<Logger>(Logger);
// l1 === l2Using symbols (recommended for interfaces)
import { Container } from 'plain-ioc';
interface Config {
baseUrl: string;
}
const TOKENS = {
Config: Symbol('Config')
} as const;
const c = new Container();
c.bindSingleton<Config>(TOKENS.Config, () => ({ baseUrl: 'https://api.example.com' }));
const cfg = c.resolve<Config>(TOKENS.Config);Wiring dependencies together
import { Container } from 'plain-ioc';
const TOKENS = {
BaseUrl: Symbol('BaseUrl')
} as const;
class ApiClient {
public constructor(public readonly baseUrl: string) {}
}
const c = new Container();
c.bindSingleton<string>(TOKENS.BaseUrl, () => 'https://api.example.com');
c.bindSingleton(ApiClient, (c) => new ApiClient(c.resolve(TOKENS.BaseUrl)));
const api = c.resolve<ApiClient>(ApiClient);
console.log(api.baseUrl);Check if a key is bound
import { Container } from 'plain-ioc';
const c = new Container();
c.isBound('service'); // false
c.bind('service', () => ({ ok: true }));
c.isBound('service'); // trueUnbind
unbind() removes the factory. If the binding was a singleton, its cached instance is removed as well.
import { Container } from 'plain-ioc';
const c = new Container();
c.bindSingleton('app', () => ({ name: 'demo' }));
c.unbind('app');
// c.resolve('app') will now throwCircular dependency detection
By default, circular dependency detection is off.
Enable it when creating the container:
import { Container } from 'plain-ioc';
const c = new Container({ circularDependencyDetect: true });When enabled, resolve() tracks a stack of keys being resolved. If the same key appears twice in the stack, a PlainIocCircularDependencyError is thrown with a message that includes the dependency stack.
Recommendation: Keep this enabled in development/test, and turn it off in production for minimal overhead.
API reference
new Container(options?)
Creates a container.
options.circularDependencyDetect?: boolean— defaultfalse
container.bind<T>(key, factory)
Registers a transient factory.
- Throws
PlainIocFactoryAlreadyBoundErrorif the key is already registered.
container.bindSingleton<T>(key, factory)
Registers a singleton factory.
- Throws
PlainIocFactoryAlreadyBoundErrorif the key is already registered.
container.resolve<T>(key): T
Resolves an instance.
- Throws
PlainIocFactoryNotBoundErrorif the key is not registered. - Throws
PlainIocCircularDependencyErrorwhen circular dependency detection is enabled and a cycle is detected.
container.unbind(key)
Removes a registered factory and clears any cached singleton instance.
- Throws
PlainIocFactoryNotBoundErrorif the key is not registered.
container.isBound(key): boolean
Returns true if a factory is registered for key.
Errors
All library errors extend the base class PlainIocError:
PlainIocErrorPlainIocFactoryNotBoundErrorPlainIocFactoryAlreadyBoundErrorPlainIocCircularDependencyError
You can catch these specifically:
import { Container, PlainIocFactoryNotBoundError } from 'plain-ioc';
const c = new Container();
try {
c.resolve('missing');
} catch (e) {
if (e instanceof PlainIocFactoryNotBoundError) {
console.error('Not registered');
}
}Testing
From a repository checkout with development dependencies installed:
npm testnpm test builds the library once, then checks the public TypeScript type contract and runs the serial Mocha/Chai
runtime suite. Packaging remains build-only, so run the tests explicitly. The suite uses the development runtime and is
not a Node.js 6 compatibility matrix.
npm run test:types and npm run test:runtime run only their respective checks and do not build the library. Before
running npm run test:runtime directly, run npm run build so dist is current.
Notes & limitations
- Factories are synchronous (the type system rejects
Promise-returning factories). If you need async initialization, create the instance elsewhere and bind it as a singleton value via a factory like() => alreadyInitialized. - There is a single container scope (no built-in child containers / scopes).
License
MIT
