@art-ws/di
v2.0.42
Published
Dependency injection for TypeScript
Downloads
95
Readme
@art-ws/di
Lightweight, TypeScript-first dependency injection inspired by Angular but framework agnostic. Works in Node ESM/moduleResolution: nodenext projects and supports scoped injectors via AsyncLocalStorage.
Installation
pnpm add @art-ws/diConcepts
Injector: resolves and caches instances; can be nested.InjectionToken<T>: opaque token for non-class providers, optionally with a factory.- Providers (Angular-like):
useClass,useFactory,useValue,useExisting,deps,multi. - Modules:
DIModuleDefwithimportsandprovidersfor ergonomic composition. - Scopes:
Injector.runScopewires a child injector toAsyncLocalStoragesoinject()resolves within the active scope.
Quick start
import { Injector, InjectionToken, inject } from "@art-ws/di"
class Config {
constructor(readonly baseUrl = "https://api.service") {}
}
class Api {
constructor(private cfg: Config) {}
fetch(path: string) {
return `${this.cfg.baseUrl}${path}`
}
}
const TOKEN_MESSAGE = new InjectionToken<string>("MESSAGE", () => "hello")
const injector = new Injector()
Injector.root = injector
injector.addProviders(
Config,
Api,
{ provide: TOKEN_MESSAGE, useFactory: () => "hi there" }
)
const api = inject(Api)
console.log(api.fetch("/ping")) // https://api.service/ping
console.log(inject(TOKEN_MESSAGE)) // hi thereUsing modules
import type { DIModuleDef } from "@art-ws/di"
import { Injector, inject } from "@art-ws/di"
class Logger { log(msg: string) { console.log(msg) } }
class Service { constructor(private logger: Logger) {} run() { this.logger.log("ok") } }
const CoreModule: DIModuleDef = {
name: "CoreModule",
providers: [Logger, Service],
}
const injector = new Injector()
Injector.root = injector
injector.addModule(CoreModule)
inject(Service).run()Scoped work (AsyncLocalStorage)
import { Injector, inject } from "@art-ws/di"
const root = new Injector()
Injector.root = root
await root.runScope(async () => {
// child scope injector is active inside this async context
const scoped = inject(Injector)
console.log(scoped === root) // false
})Utility helpers
runScopedTask({ task, deps, providers }): convenience to resolve deps and dispose scoped instances.disposeInstances(instances): callsdispose()on scoped instances; errors are collected.getModuleName(module): best-effort name for diagnostics.
Provider shapes (reference)
// Class provider
injector.addProviders(MyClass)
// Value
injector.addProviders({ token: TOKEN, factory: () => value })
// Angular-like
injector.addProviders({
provide: TOKEN,
useFactory: (a, b) => create(a, b),
deps: [DepA, DepB],
})
injector.addProviders({ provide: TOKEN, useValue: 42 })
injector.addProviders({ provide: TOKEN, useExisting: OtherToken })
injector.addProviders({ provide: TOKEN, useClass: Impl, deps: [DepA] })TypeScript notes
- Published as ESM with type definitions; import with explicit
.jspaths in NodeNext if using path-mapped sources. InjectionToken<T>can carry afactoryused when no other provider exists.DepsFor<C>enforces constructor arity when declaringuseClassdeps.
Testing
The project uses vitest. To run tests locally:
pnpm testLicense
UNLICENSED (see package.json).
