@veren/di
v0.0.0
Published
A starter for creating a TypeScript package.
Readme
@veren/di
@veren/di is a small dependency injection container with:
InjectionTokenfor non-class dependencies@Injectable()for auto-provided classesInjectorandEnvironmentInjectorfor provider resolution and lifecycleinject()andrunInInjectionContext()for functional injectionreflect-metadatabased constructor type inference for class dependenciesforwardRef()for deferred token references@Inject(),@Optional(),@Self(),@SkipSelf(), and@Host()parameter metadata helpers
Install
vp add @veren/diQuick Start
import {
EnvironmentInjector,
Inject,
Injectable,
InjectionToken,
Injector,
inject,
runInInjectionContext,
} from "@veren/di";
const API_URL = new InjectionToken<string>("API_URL", {
factory: () => "https://example.com/api",
});
class HttpClient {
constructor(@Inject(API_URL) readonly baseUrl: string) {}
}
@Injectable({ providedIn: "root" })
class UserService {
readonly apiUrl = inject(API_URL);
constructor(@Inject(HttpClient) readonly http: HttpClient) {}
}
const injector = Injector.create({
providers: [HttpClient],
});
const userService = injector.get(UserService);
const apiUrl = runInInjectionContext(injector as EnvironmentInjector, () => inject(API_URL));Core Concepts
Tokens
Classes can be used directly as tokens. For non-class dependencies, prefer InjectionToken<T>:
const CONFIG = new InjectionToken<{ baseUrl: string }>("CONFIG");An InjectionToken can also declare a default factory:
const REQUEST_ID = new InjectionToken("REQUEST_ID", {
providedIn: "any",
factory: () => crypto.randomUUID(),
});Injectable
@Injectable() registers an InjectableDef so the container can auto-create the class:
@Injectable({ providedIn: "root" })
class Logger {}Supported providedIn values:
"root": one shared instance from the root injector"platform": currently behaves the same as"root""any": one cached instance per injectornullor omitted: not auto-provided, must be registered manually
Providers
Supported provider forms:
Injector.create({
providers: [
ServiceClass,
{ provide: TOKEN, useValue: 123 },
{ provide: TOKEN, useExisting: OTHER_TOKEN },
{ provide: TOKEN, useFactory: () => createValue(), deps: [DepA, DepB] },
{ provide: TOKEN, useClass: ServiceImpl, deps: [DepA] },
{ provide: ServiceClass, deps: [DepA, DepB] },
{ provide: HOOKS, useValue: "a", multi: true },
{ provide: HOOKS, useValue: "b", multi: true },
],
});multi: true aggregates all providers for the same token into an array.
Constructor Injection
Class dependencies can be inferred from runtime metadata when the class is decorated and TypeScript emits design:paramtypes:
@Injectable()
class Logger {}
@Injectable()
class ApiClient {
constructor(readonly logger: Logger) {}
}@Inject(...) still has higher priority and should be used for non-class dependencies, primitives, interfaces, or when you want to override reflected types:
class ApiClient {
constructor(@Inject(API_URL) readonly url: string) {}
}You can also skip class metadata and declare deps on the provider directly:
{
provide: ApiClient,
useClass: ApiClient,
deps: [API_URL],
}Resolution Flags
Both parameter decorators and deps arrays support these flags:
Optional: returnnullwhen the dependency is missingSelf: only search the current injectorSkipSelf: start searching from the parent injectorHost: currently recorded as a flag, with no special resolution behavior
Example with deps markers:
{
provide: TOKEN,
useFactory: (local, parent, fallback) => ({ local, parent, fallback }),
deps: [
[LOCAL_TOKEN, Self],
[PARENT_TOKEN, SkipSelf],
[MISSING_TOKEN, Optional],
],
}Functional Injection
Use inject() inside an active injection context:
const injector = Injector.create({ providers: [] });
const value = runInInjectionContext(injector as EnvironmentInjector, () => inject(API_URL));Calling inject() outside an injection context throws. assertInInjectionContext() can be used for explicit guards.
forwardRef
Use forwardRef() when a token must be resolved later:
class A {
constructor(@Inject(forwardRef(() => B)) readonly b: B) {}
}
class B {
constructor(@Inject(forwardRef(() => A)) readonly a: A) {}
}TypeScript Config
Decorator syntax and runtime constructor inference rely on legacy TypeScript decorators:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}reflect-metadata is bundled as a runtime dependency by this package.
Lifecycle
The injector tracks instances that implement either of these hooks and calls them during destroy():
ngOnDestroy()dispose()
EnvironmentInjector.destroy() also destroys child injectors recursively.
Errors
Common error messages:
- missing provider:
No provider for ... - circular dependency:
Circular dependency detected for ... - missing constructor token metadata:
Missing injection token for parameter ... inject()outside a context:inject() must be called from an injection context.
Development
vp test
vp check