Injection
Prefer the helper functions over building an InjectionDescriptor manually — they are
more concise, composable, and less error-prone.
All helpers are exported individually or through the inject namespace:
// named imports
import { allOf, optional, provide, mapped, object, defer, useValue } from '@caffeine-projects/dicaf'
// namespace import — all helpers available as inject.*
import { inject } from '@caffeine-projects/dicaf'
inject.allOf(Plugin)
inject.optional(Logger)
inject.provide(EmailSender)
Injection type
type Injection<T = unknown> = Key<T> | InjectionDescriptor<T>
Every place that accepts a dependency specification accepts either a bare Key
or a full InjectionDescriptor. Helpers like optional() and allOf() return
InjectionDescriptor values that you pass in the same position.
@Injectable([Logger, optional(Database), allOf(Plugin)])
class App { ... }
InjectionDescriptor
type InjectionDescriptor<T = any> = {
key?: Key<T> // the dependency key
multiple?: boolean // inject all bindings for the key (returns T[])
optional?: boolean // ok if missing — injects undefined instead of throwing
resolver?: symbol // custom resolver (overrides the default)
args?: unknown // extra arguments passed to the resolver
}
Helpers
allOf
allOf(keyOrDescriptor: Key | InjectionDescriptor): InjectionDescriptor
Injects all bindings registered for a key as an array. This is the injection
equivalent of di.getMany().
abstract class Validator {
abstract validate(value: unknown): boolean
}
@Injectable()
class RequiredValidator extends Validator { ... }
@Injectable()
class MaxLengthValidator extends Validator { ... }
@Injectable([allOf(Validator)])
class Pipeline {
constructor(readonly validators: Validator[]) {}
}
optional
optional(keyOrDescriptor: Key | InjectionDescriptor): InjectionDescriptor
Marks a dependency as optional. If no binding is registered for the key, the
container injects undefined instead of throwing.
@Injectable([optional(FeatureFlags)])
class UserService {
constructor(private readonly flags?: FeatureFlags) {}
}
Can be combined with other helpers:
@Injectable([optional(allOf(Plugin))])
provide
provide(keyOrDescriptor: Key | InjectionDescriptor): InjectionDescriptor
Wraps the resolved dependency in a Provider<T>. The provider's get() method
resolves the dependency on each call, creating a fresh instance for transient
scopes. Use this to inject a shorter-lived dependency into a longer-lived one
without a scope violation.
interface Provider<T> {
get(): T
}
@Injectable([provide(TransientEmailSender)])
@Lifetime(Scopes.SINGLETON)
class NotificationService {
constructor(private readonly sender: Provider<TransientEmailSender>) {}
send(msg: string) {
this.sender.get().send(msg) // new instance each call
}
}
mapped
mapped(key: Key): InjectionDescriptor
Injects all bindings for key as a Map<string, T>, where the map key is
the binding's name. Useful when you need to look up bindings by name at
runtime.
@Injectable()
@Named('horror')
class HorrorMovie implements Movie { ... }
@Injectable()
@Named('comedy')
class ComedyMovie implements Movie { ... }
@Injectable([mapped('movie')])
class MovieService {
constructor(readonly movies: Map<string, Movie>) {}
// movies.get('horror') → HorrorMovie instance
}
object
object(spec: ObjectInjectionSpec): InjectionDescriptor
Injects multiple dependencies into a single constructor parameter as an object.
The spec argument maps property names to injection keys or
descriptors.
type ObjectInjectionSpec = {
[prop: string | symbol]: Key | InjectionDescriptor | ObjectInjectionSpec
}
@Injectable([object({ db: Database, logger: optional(Logger) })])
class UserService {
constructor(readonly deps: { db: Database; logger?: Logger }) {}
}
defer
defer(keyFn: () => Key): InjectionDescriptor
Defers key resolution until the container constructs the instance. Use this
when a circular module import would cause the key to be undefined at class
declaration time.
import { DeferredCtor, defer } from '@caffeine-projects/dicaf'
@Injectable([defer(() => B)])
class A {
constructor(private readonly b: B) {}
}
@Injectable([A])
class B {
constructor(private readonly a: A) {}
}
For cases where the key is a class reference in a circular module, prefer
new DeferredCtor(() => ClassName) passed directly in the @Injectable deps array.
useValue
useValue<T>(value: T): InjectionDescriptor
Injects a constant value directly, without a container binding.
@Injectable([useValue('localhost'), useValue(5432)])
class DatabaseClient {
constructor(readonly host: string, readonly port: number) {}
}
compose
compose(key: Key, ...fns: Array<(key: Key) => InjectionDescriptor>): InjectionDescriptor
Composes multiple injection modifier functions around a single key. Applies each function's result to the descriptor from left to right.
const injectOptionalMany = (key: Key) => compose(key, optional, allOf)