Skip to content

Modules ​

Use .use() to split a large container builder into smaller pieces while keeping type inference across the fluent chain.

ts
const appContainer = new Container()
  .registerValue('config', { env: 'production' as 'production' | 'test' })
  .use((c) => c.registerClass('db', Database, []))
  .use((c) => {
    const { env } = c.get('config')
    return env === 'test'
      ? c.registerClass('mailer', MockMailer, [])
      : c.registerClass('mailer', RealMailer, [])
  })

Inline lambdas are the most ergonomic shape. The lambda's container type is inferred from the call site, including keys registered earlier in the chain.

Named Modules ​

Reusable named modules declare only what they require and provide with Module<TRequirements, TProvides>. The actual graph may contain additional registrations; they are preserved in the result.

ts
import {
  Container,
  type Module,
  type SpecMap
} from '@inferdi/inferdi'

type Requirements = SpecMap<{ config: { env: string } }>
type Provides = SpecMap<{ mailer: Mailer }>

const addMailer: Module<Requirements, Provides> = (c) => {
  const { env } = c.get('config')
  return env === 'test'
    ? c.registerClass('mailer', MockMailer, [])
    : c.registerClass('mailer', RealMailer, [])
}

const app = new Container()
  .registerValue('config', { env: 'test' })
  .registerValue('metrics', new Metrics())
  .use(addMailer) // keeps config + metrics and adds mailer

The callback sees only Container<TRequirements>. Requirements must match by service type, exact lifetime, sync/async mode, managed-lazy mode, and scope-input readiness. Outputs cannot collide with any actual key. Missing requirements, incompatible requirements, and output collisions produce named diagnostics.

Dynamic Imports ​

Use import() when an optional or route-specific module should load in a separate JavaScript chunk.

ts
const { reportsModule } = await import('./reports.module')
const container = new Container().use(reportsModule)

The runtime loads and evaluates reports.module before .use() runs. .use() then executes the module synchronously, adds its registrations, and returns the inferred container type. A dynamic import does not make those services asynchronous; use registerAsyncFactory when service initialization itself is asynchronous.

In a browser, use this pattern at a route or feature boundary, and only when your bundler emits a separate chunk that nothing else imports statically. Build the feature container after that chunk loads. On a backend, prefer static imports during normal startup. A dynamic import is useful for deployment-selected optional features or cold-start-sensitive serverless paths whose unused code and dependencies should remain unloaded. In either runtime, assemble the container before its first resolution or createScope(); do not mutate the application container per request.

For keys selected at runtime, use the .has() type guard before resolution.

Compiler Check ​

A named module cannot be installed until its declared requirements are present:

ts
import { 
Container
, type
Module
, type
SpecMap
} from '@inferdi/inferdi'
type
Requirements
=
SpecMap
<{
config
: {
env
: string } }>
type
Provides
=
SpecMap
<{
feature
: string }>
const
addFeature
:
Module
<
Requirements
,
Provides
> = (
container
) =>
container
.
registerFactory
('feature', (
c
) =>
c
.
get
('config').
env
, ['config'])
new
Container
().
use
(
addFeature
)
const
app
= new
Container
()
.
registerValue
('config', {
env
: 'test' })
.
use
(
addFeature
)
const
feature
=
app
.
get
('feature')
const feature: string