async-injection
v3.0.0
Published
A robust lightweight dependency injection library for TypeScript.
Maintainers
Readme
Async-Injection
Lightweight TypeScript dependency injection — with first-class async support.
Most DI containers assume your dependencies are ready the moment they are constructed. async-injection doesn't.
Synchronous and asynchronous dependencies can coexist naturally in the same container, and the library resolves each correctly — whether you get them immediately or need to await them.
Install
npm install async-injectionWorks in Node, browsers, Electron, and other runtimes.
Ships as both ESM and CJS side by side.
Quick start
@Injectable()
class SharedService {
constructor(@Inject('LogLevel') @Optional('warn') private logLevel: string) { }
}
@Injectable()
class TransactionHandler {
constructor(svc: SharedService) { }
}
const container = new Container();
container.bindClass(SharedService).asSingleton(); // one shared instance
container.bindClass(TransactionHandler); // new instance on each get
container.bindConstant('LogLevel', 'info'); // override defaulted 'warn' level
const tx = container.get(TransactionHandler);Tip:
Real-world projects should follow best practices like separation of concerns, having a composition root, and should avoid anti-patterns like service locator.
Setup
Two tsconfig.json settings are required:
{
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}Reflection metadata is also required. Rather than mandate a specific library, you have the freedom to bring your own — choose whichever fits your project:
Import it once at your entry point, before anything else:
import 'reflect-metadata';Async dependencies
Synchronous injection is straightforward and well understood.
Asynchronous injection is also well established.
But when you are blending the two in the same container, it requires a little care.
get vs resolve
Think of get(X) / resolve(X) as a request not just for X, but for the entire tree of objects X depends on.get is only safe when every node in that tree is already settled.
| Condition | When to use |
|---|---|
| All dependencies are synchronous, or async singletons are already resolved | container.get(X) |
| Any dependency in the tree may still be pending | await container.resolve(X) |
Tip:
CallresolveSingletons(true)after your lastbindXXXcall and before anygetcall to avoid hard-to-debug timing issues.
When a dependency must do async work before it is usable — open a database connection, load remote config, etc. — there are two ways to handle it:
Async factory — bind an async factory that performs the initialization and returns the ready instance:
container.bindAsyncFactory(SharedService, async () => {
const svc = new SharedService();
return svc.connect(); // returns Promise<SharedService>
}).asSingleton();
// Option A — resolve everything up front, then use get() as normal
await container.resolveSingletons(true);
const tx = container.get(TransactionHandler);
// Option B — resolve on demand
const tx = await container.resolve(TransactionHandler);Note:
A factory takes full responsibility for constructing and initializing its object —@PostConstructis not called on factory-returned instances.bindFactoryandbindAsyncFactoryare therefore the right choice when you need complete control over how an object is built, or when you cannot annotate the class.
@PostConstruct — mark an initialization method to run on the fully constructed object after the constructor returns.
The method can be synchronous or asynchronous, which is especially useful since a class constructor can never be async.
It is also useful because a base class constructor cannot call methods overridden by a subclass.
The method can have parameters which can be annotated with @Inject and @Optional — the container resolves and injects them before calling the method.
This lets you avoid storing dependencies from the constructor solely for post-construction use:
@Injectable()
class DatabasePool {
@PostConstruct()
async init(@Inject(DbConfig) config: DbConfig): Promise<void> {
this.pool = await createPool(config); // config is injected, not stored
}
}Important:
Always explicitly declare the return type (voidorPromise<void>, never leave it to be inferred).container.get()will throw if the return type is missing and the method actually does return a Promise.
Constructor and@PostConstructparameters follow the same rules: class-typed params are auto-resolved by reflected type; use@Injectfor interface or primitive types. Use@Optional()with no argument to passundefinedif you want to allow a JS parameter default.
Scopes
Create isolated or hierarchical scopes using multiple containers.
A child container searches its own bindings first, then walks up the parent hierarchy:
const child = new Container(parent);IoC modules
No special module system needed — TypeScript's own import is your module system. Create a file, import your container, and register your bindings.
API
A Container's life follows a simple arc: configure it by registering bindings, activate it so async singletons can initialize, then use it to retrieve objects.
Configure
| | |
|---|----------------------------------------------------------------|
| new Container(parent?) | Create a container; optionally inherit bound ids from a parent |
| bindConstant(id, value) | Bind a fixed value |
| bindClass(id, class?) | Bind a class (requires @Injectable) |
| bindFactory(id, fn) | Bind a synchronous factory function |
| bindAsyncFactory(id, fn) | Bind an asynchronous factory function |
| .asSingleton() | Chain: share one instance across the Container |
| .onError(cb) | Chain: handle construction errors |
Activate
| | |
|---|---|
| resolveSingletons(true) | Await all async singleton initializations |
Use
| | |
|---|---|
| get(id) | Synchronously retrieve a bound value |
| resolve(id) | Asynchronously retrieve a bound value (see get vs resolve) |
Annotate your classes
| | |
|---|---|
| @Injectable() | Required on any class bound with bindClass |
| @Inject(id) | Explicitly declare which id to inject into a constructor parameter |
| @Optional(default?) | Provide a fallback if the id is not bound; omit the argument to let a JS parameter default apply |
| @PostConstruct() | Mark a method to run after full construction (sync or async); parameters annotated with @Inject/@Optional are injected by the container |
| @Release() | Mark a method to call when a singleton is released |
| InjectionToken<T> | Create a typed token for binding interfaces or primitives |
Acknowledgements
Inspired by InversifyJS, NestJS async providers, Darcy Rayner's DI walkthrough, and Carlos Delgado's QueryablePromise idea.
Support Resources
The support/ directory contains supplementary guides that are not part of the library itself:
lazy-loading/— patterns for on-demand, split-bundle DI module loadingreact-integration/— using with React applications, including scoped child containers and testing patternsmigrate-from-inversify/— shim files and a two-phase migration guide for InversifyJS usersmigrate-from-tsyringe/— migration guide for TSyringe usersmigrate-from-typedi/— migration guide for TypeDI users
License
MIT © 2020–2024 Frank Stock
