Skip to main content

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)