Bindings
Bindings connect tokens to providers. Start with bind(token), choose a provider, and optionally set a lifetime or a disposer.
Factory Bindings
Factories are lazy and infer their result type from the token.
Call shapes:
bind(token).factory(factory)
bind(token).factory(dependencies, factory)import { bind } from "@satunnaisuus/distill";
const configBinding = bind(Config).factory(() => ({ port: 3000 }));Use a dependency map when the factory needs services from the container:
const serverBinding = bind(Server).factory({ config: Config, logger: Logger }, ({ config, logger }) => ({
start: () => logger.log(`Listening on ${config.port}`),
}));Dependency map values can be tokens, optional(...), or ref(...).
Value, Class, and Alias Providers
Use .value(...) for already-created values:
Call shape:
bind(token).value(value)const configBinding = bind(Config).value({ port: 3000 });Use .class(...) for class construction. Without dependencies, Distill calls the class constructor with no arguments. With dependencies, Distill passes the resolved dependency object as the single constructor argument.
Call shapes:
bind(token).class(Class)
bind(token).class(dependencies, Class)class ClockImpl {
now() {
return new Date();
}
}
class ServerImpl {
constructor(private readonly services: { readonly config: Config; readonly logger: Logger }) {}
}
const clockBinding = bind(Clock).class(ClockImpl);
const serverBinding = bind(Server).class({ config: Config, logger: Logger }, ServerImpl);Use .alias(...) or .useExisting(...) when one token should resolve another token's value.
Call shapes:
bind(token).alias(existingToken)
bind(token).useExisting(existingToken)const loggerBinding = bind(Logger).alias(ConsoleLogger);Lifetimes
Set lifetime before or after the provider method:
Call shapes:
bind(token).singleton()
bind(token).scoped()
bind(token).transient()
bind(token).singleton().factory(...)
bind(token).factory(...).scoped()const dbBinding = bind(Db).singleton().factory(() => createDb());
const currentUserBinding = bind(CurrentUser).factory(() => currentUser).scoped();
const requestIdBinding = bind(RequestId).transient().factory(() => crypto.randomUUID());The lifetimes are:
singleton: cached in the scope where the binding is registered.scoped: cached in the scope that resolves the service.transient: not cached.
Factory, value, and class bindings are singleton by default. Alias bindings are transient by default so the alias follows the existing token's lifetime instead of caching a separate value.
Singleton bindings cannot depend on scoped bindings.
Optional Dependencies
Wrap a dependency with optional(...) when absence is intentional.
Call shapes:
optional(dependency)
optional(() => dependency)import { optional } from "@satunnaisuus/distill";
const serverBinding = bind(Server).factory({ config: optional(Config) }, ({ config }) => ({
port: config?.port ?? 3000,
}));The factory receives undefined when the dependency is not visible in the current container or scope.
Use optional(() => dependency) when the dependency reference itself should be selected lazily.
const maybeConfig = optional(() => Config);Lazy References
Use ref(...) when access can be delayed, when a dependency is expensive, or when two services need to refer to each other after initialization.
Call shapes:
ref(token)
ref(() => token)import { ref } from "@satunnaisuus/distill";
const jobRunnerBinding = bind(JobRunner).factory({ logger: ref(Logger) }, ({ logger }) => ({
run: () => logger.value.log("Running job"),
}));The target service is resolved when .value is read.
Use ref(() => token) when the target token should be selected lazily. The service is still not resolved until .value is read.
const selectedLogger = ref(() => (useJson ? JsonLogger : TextLogger));Multibind Dependencies
Use multiToken(...) for a token with multiple contributions. In dependency maps, use the multibind token directly to inject all visible contributions.
Call shapes:
multiToken(key).of<T>()
container.resolve(multibindToken)import { bind, multiToken } from "@satunnaisuus/distill";
const Hooks = multiToken("Hooks").of<{ readonly run: () => void }>();
const registryBinding = bind(Registry).factory({ hooks: Hooks }, ({ hooks }) => ({
run: () => hooks.forEach((hook) => hook.run()),
}));Resolve multibind tokens with resolve(...).
const hooks = container.resolve(Hooks);Disposal
Attach a disposer to close values owned by a binding.
Call shapes:
bind(token).factory(...).disposable(disposer)
bind(token).disposable(disposer).factory(...)
container.dispose()const dbBinding = bind(Db)
.singleton()
.factory(() => createDb())
.disposable((db) => db.close());Disposing a container or scope closes owned disposable values. Parent disposal cascades to child scopes before closing parent-owned instances.