Skip to content

Factories ​

Use registerClass when construction is exactly new Ctor(...deps). Use registerFactory when construction needs configuration, a third-party API, an interface binding, or other explicit logic.

ts
const container = new Container()
  .registerValue('config', {
    dsn: 'postgres://localhost/app',
    poolSize: 10
  })
  .registerFactory('pool', (c) => {
    const { dsn, poolSize } = c.get('config')
    return new Pool({ connectionString: dsn, max: poolSize })
  })
  .registerClass('users', UserRepository, ['pool'])

The return type becomes the registered service type.

Container-aware Factories ​

The basic callback receives a lifetime-filtered container. A singleton factory can resolve only singleton-safe dependencies; scoped and transient keys are rejected by TypeScript.

ts
const root = new Container()
  .registerValue('prefix', 'app')
  .registerFactory('logger', (c) => new Logger(c.get('prefix')))

Use this form when the factory needs conditional or multi-step resolution. Keep all .get() calls synchronous. For an initialization boundary that should propagate through the graph, use registerAsyncFactory instead.

Declared Factory Dependencies ​

The deps-aware overload makes factory requirements visible in the graph and limits the callback resolver to those keys:

ts
const root = new Container()
  .declareScopeInputs<{ request: RequestContext }>()
  .registerClass('logger', Logger, [])
  .registerFactory(
    'requestLog',
    (deps) => new RequestLog(
      deps.get('request'),
      deps.get('logger')
    ),
    ['request', 'logger'],
    'scoped'
  )

Declared dependencies carry scope-input readiness and lifetime requirements through modules and other registrations. Unlike registerAsyncFactory, this callback receives a resolver, not positional values.

Factory Lifetimes ​

Factories use the same lifetimes as classes:

ts
const root = new Container()
  .registerFactory('cache', () => new Cache(), 'singleton')
  .registerFactory('requestState', () => new RequestState(), 'scoped')
  .registerFactory('operation', () => new Operation(), 'transient')

Singleton and scoped results are cached and owned by their container. Transient results are not cached or disposed by InferDI.

Pass a fourth lazyKey to create a lifetime-preserving companion:

ts
const root = new Container()
  .registerFactory('cache', () => new Cache(), 'singleton', 'cacheLazy')

root.get('cacheLazy').get()

A companion requires an explicit lifetime, including 'singleton'. See Lazy Injection for lifetime and disposal rules.

Bind Interfaces ​

Interfaces have no runtime constructor. Give the factory an explicit service type when consumers should depend on an abstraction:

ts
interface Mailer {
  send(message: string): void
}

class SendGridMailer implements Mailer {
  send(message: string) {}
}

const container = new Container()
  .registerFactory<'mailer', Mailer>(
    'mailer',
    () => new SendGridMailer()
  )

Consumers of mailer now see Mailer, not SendGridMailer.

Choose the Promise Contract ​

A Promise returned by registerFactory is the service value itself: .get() returns Promise<T> and dependent factories receive that same Promise. Use this only when the Promise belongs in the synchronous graph.

Use registerAsyncFactory when T is the service and the Promise is its initialization boundary. That form propagates async status and resolves through .getAsync(). Async Dependencies documents both contracts, caching, lazy companions, failure behavior, and teardown.