plain-ioc
v1.0.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 {
log(message: string) {
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 {
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 CircularDependencyError 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
FactoryAlreadyBoundErrorif the key is already registered.
container.bindSingleton<T>(key, factory)
Registers a singleton factory.
- Throws
FactoryAlreadyBoundErrorif the key is already registered.
container.resolve<T>(key): T
Resolves an instance.
- Throws
FactoryNotBoundErrorif the key is not registered. - Throws
CircularDependencyErrorwhen circular dependency detection is enabled and a cycle is detected.
container.unbind(key)
Removes a registered factory and clears any cached singleton instance.
- Throws
FactoryNotBoundErrorif 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:
PlainIocErrorFactoryNotBoundErrorFactoryAlreadyBoundErrorCircularDependencyError
You can catch these specifically:
import { Container, FactoryNotBoundError } from "plain-ioc";
const c = new Container();
try {
c.resolve("missing");
} catch (e) {
if (e instanceof FactoryNotBoundError) {
console.error("Not registered");
}
}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
