@vielzeug/wireit
v2.1.0
Published
> Lightweight typed dependency injection container for TypeScript with tokens, lifetimes, async resolution, child containers, and test utilities
Readme
@vielzeug/wireit
Lightweight typed dependency injection container for TypeScript with tokens, lifetimes, async resolution, child containers, and test utilities
Wireit is a zero-dependency IoC container built around typed tokens — register and resolve dependencies with singleton/transient/scoped lifetimes, async factories, dispose hooks, child containers, and first-class test helpers.
Installation
pnpm add @vielzeug/wireit
# npm install @vielzeug/wireit
# yarn add @vielzeug/wireitQuick Start
import { createContainer, createToken } from '@vielzeug/wireit';
const DbToken = createToken<Database>('Database');
const ServiceToken = createToken<UserService>('UserService');
const container = createContainer();
container
.factory(DbToken, () => new Database(process.env.DB_URL!))
.bind(ServiceToken, UserService, { deps: [DbToken] });
const service = container.get(ServiceToken);Minimal API (Recommended for New Code)
For a cleaner, more ergonomic development experience, use the simplified convenience methods:
import { createContainer, createToken } from '@vielzeug/wireit';
const DbToken = createToken<Database>('Database');
const ServiceToken = createToken<UserService>('UserService');
const container = createContainer();
// Auto-detects whether it's a class, factory, or value
container.set(DbToken, Database, { deps: [DbToken] });
container.set(ServiceToken, UserService, { deps: [DbToken] });
// Always async — handles both sync and async providers uniformly
const service = await container.resolve(ServiceToken);
const [db, cache] = await container.resolveAll([DbToken, CacheToken]);
// Optional resolution
const optional = await container.resolveOptional(MissingToken); // undefinedFeatures
- ✅ Typed tokens —
createToken<T>(description)— type-safe, no magic strings - ✅ Three registration styles —
register(),factory()shorthand,bind()shorthand - ✅ Plain values —
container.value(token, value)for constants and config - ✅ Lifetimes —
singleton(default),transient, andscopedper-child-container - ✅ Async providers — factories may return
Promise<T>; resolve withgetAsync() - ✅ Dispose hooks — per-provider
dispose(instance)called oncontainer.dispose() - ✅ Child containers —
createChild()inherits registrations;scopedlifetime creates one instance per child - ✅ Scoped execution —
runInScope(fn)creates, uses, and auto-disposes a child - ✅ Aliases —
alias(token, source)maps one token to another, resolved through parent chain - ✅ Batch resolution —
getAll([...tokens])andgetAllAsync([...tokens])return typed tuples - ✅ Optional resolution —
getOptional()andgetOptionalAsync()returnundefinedwhen missing - ✅ Test helpers —
createTestContainer(base?)andcontainer.mock(token, mock, fn) - ✅ Snapshot/Restore —
snapshot()andrestore(snap)for stateless test isolation - ✅ Debug —
debug()walks the full parent chain and lists all tokens and aliases - ✅ Zero dependencies — <2 kB gzipped, pure TypeScript ESM
Recent Improvements (v2+)
- Resource cleanup — Dispose hooks now called for replaced and restored instances, preventing resource leaks
- Async failure retry — Failed async singleton resolutions can now be retried (no sticky failures)
- Consistent lifecycle —
clear()now properly guards disposed state for consistency - Minimal API surface — New
set(),resolve(),resolveAll(),resolveOptional()methods for cleaner, more ergonomic code - Improved internals — Removed module-scoped factory indirection; cleaner static factory methods
Usage
Tokens
Every dependency is identified by a typed token, not a string or class reference:
import { createToken } from '@vielzeug/wireit';
const ConfigToken = createToken<AppConfig>('AppConfig');
const DbToken = createToken<Database>('Database');
const ServiceToken = createToken<UserService>('UserService');The description is required — it appears in error messages and debug() output.
Registering Providers
Value
Use value() for constants, config, and plain objects:
container.value(ConfigToken, { apiUrl: 'https://api.example.com', timeout: 5000 });Factory
Use factory() for any function that creates an instance:
container.factory(DbToken, (config) => new Database(config.apiUrl), {
deps: [ConfigToken],
});Class (bind)
Use bind() to pair a class directly with a token:
container.bind(ServiceToken, UserService, { deps: [DbToken] });Full register
Use register() for the full provider object — useful when the provider type matters:
container.register(ServiceToken, { useClass: UserService, deps: [DbToken], lifetime: 'transient' });Lifetimes
| Lifetime | Behaviour |
| --------------------- | -------------------------------------------------------- |
| singleton (default) | One instance per container — created on first get() |
| transient | New instance on every get() |
| scoped | One instance per child container; singletons in the root |
container.factory(RequestId, () => crypto.randomUUID(), { lifetime: 'transient' });
container.bind(RequestHandler, RequestHandlerImpl, { deps: [RequestId], lifetime: 'scoped' });Child Containers and Hierarchy
Child containers inherit all registrations from their parent. Registrations in the child take precedence:
const child = container.createChild();
child.value(RequestContext, { userId: 'u1' });
const handler = child.get(RequestHandler); // uses child contextScoped Execution
runInScope() creates a child container, passes it to your function, then disposes it automatically:
await container.runInScope(async (scope) => {
scope.value(RequestId, crypto.randomUUID());
const handler = scope.get(RequestHandler);
await handler.process(request);
});Async Resolution
Factories can return a Promise<T>. Use getAsync() (or getAllAsync()) to resolve them:
container.factory(DbToken, async () => {
const db = new Database(env.DB_URL);
await db.connect();
return db;
});
const db = await container.getAsync(DbToken);Aliases
Map one token to another — useful for interface-to-implementation bindings:
const IUserServiceToken = createToken<IUserService>('IUserService');
container.bind(UserServiceToken, UserServiceImpl, { deps: [DbToken] });
container.alias(IUserServiceToken, UserServiceToken);
const svc = container.get(IUserServiceToken); // resolves to UserServiceImplBatch Resolution
Resolve multiple tokens at once with a typed tuple result:
const [db, config, logger] = container.getAll([DbToken, ConfigToken, LoggerToken]);
const [db, cache] = await container.getAllAsync([DbToken, CacheToken]);Dispose Hooks
Register a dispose callback on any class or factory provider:
container.factory(DbToken, () => new Database(env.DB_URL), {
dispose: (db) => db.close(),
});
await container.dispose(); // calls db.close() on the cached singletondispose() is idempotent — calling it multiple times is safe. The container also implements [Symbol.asyncDispose] for await using syntax.
Testing
createTestContainer
createTestContainer(base?) returns { container, dispose } — an isolated child container for test overrides and a cleanup function:
import { createTestContainer, createToken } from '@vielzeug/wireit';
const { container, dispose } = createTestContainer(appContainer);
container.value(DbToken, mockDb, { overwrite: true });
afterEach(() => dispose());container.mock
mock() temporarily replaces a token for the duration of a callback, then restores the original:
const result = await container.mock(DbToken, fakeDb, () => service.loadUsers());
// Or with a full provider:
await container.mock(DbToken, { useFactory: () => createInMemoryDb() }, async () => {
// ... test code
});Snapshot / Restore
For lower-level test isolation:
const snap = container.snapshot();
container.value(DbToken, fakeDb, { overwrite: true });
// ... test
container.restore(snap);API
Package exports
export {
AliasCycleError,
AsyncProviderError,
CircularDependencyError,
Container,
ContainerDisposedError,
createContainer,
createTestContainer,
createToken,
ProviderNotFoundError,
} from '@vielzeug/wireit';
export type {
ClassProvider,
FactoryProvider,
Lifetime,
Provider,
ProviderOptions,
Snapshot,
Token,
TokenValues,
ValueProvider,
} from '@vielzeug/wireit';Factory functions
| Export | Description |
| ----------------------------- | ------------------------------------------------------------------------------ |
| createToken<T>(description) | Create a typed dependency injection token |
| createContainer() | Create a new root container |
| createTestContainer(base?) | Returns { container, dispose } — isolated child container for test overrides |
Container registration
| Method | Description |
| ---------------------------------- | ------------------------------------------ |
| register(token, provider, opts?) | Register a full Provider<T> |
| value(token, val, opts?) | Register a plain value |
| factory(token, fn, opts?) | Register a factory function |
| bind(token, cls, opts?) | Bind a class to a token |
| alias(token, source) | Map token to source |
| unregister(token) | Remove a registration |
| clear() | Clear all registrations (no dispose hooks) |
Container resolution
| Method | Description |
| ---------------------------- | ---------------------------------------- |
| get<T>(token) | Resolve synchronously — throws if async |
| getAsync<T>(token) | Resolve asynchronously |
| getAll(tokens) | Resolve a tuple of tokens synchronously |
| getAllAsync(tokens) | Resolve a tuple of tokens asynchronously |
| getOptional<T>(token) | Resolve or return undefined |
| getOptionalAsync<T>(token) | Resolve async or return undefined |
| has(token) | Check if a token is registered |
Container lifecycle
| Method / Property | Description |
| ----------------------- | ----------------------------------------- |
| createChild() | Create a child container |
| runInScope(fn) | Run fn in an auto-disposed child |
| dispose() | Run dispose hooks and clear registrations |
| disposed | Whether the container has been disposed |
| [Symbol.asyncDispose] | await using support |
Container testing
| Method | Description |
| ----------------------- | ------------------------------------------------- |
| mock(token, mock, fn) | Temporarily replace a token; restore after fn |
| snapshot() | Capture current registrations |
| restore(snap) | Restore a previous snapshot |
| debug() | List all tokens and aliases (including inherited) |
Exported types
| Type | Description |
| -------------------------- | ------------------------------------------------------------ |
| Token<T> | Typed injection token |
| Lifetime | 'singleton' \| 'transient' \| 'scoped' |
| Provider<T> | Union of ValueProvider, ClassProvider, FactoryProvider |
| ProviderOptions<T, Deps> | Options for factory() and bind() |
| TokenValues<T> | Extracts value types from a tuple of tokens |
| Snapshot | Opaque snapshot handle |
Exported errors
| Error | Thrown when |
| ------------------------- | -------------------------------------------- |
| ProviderNotFoundError | No provider is registered for a token |
| CircularDependencyError | A dependency graph cycle is detected |
| AsyncProviderError | An async provider is resolved with get() |
| AliasCycleError | Alias definitions form a cycle |
| ContainerDisposedError | Any method is called on a disposed container |
Documentation
Full docs at vielzeug.dev/wireit
| | | | ------------------------------------------------ | --------------------------------------- | | Usage Guide | Registration, lifetimes, async, testing | | API Reference | Complete type signatures | | Examples | Real-world DI patterns |
License
MIT © Helmuth Saatkamp — Part of the Vielzeug monorepo.
