Type Safety
InferDI carries the declared dependency graph in the container's type. Each registration adds a key, service type, lifetime, sync or async state, and any scope-input requirements. Later calls are checked against that accumulated graph type state.
Constructor Signatures
registerClass checks dependency keys against constructor parameters by position and structural assignability.
import { Container } from '@inferdi/inferdi'
class Logger {
info(message: string) {}
}
class Database {
findUser(id: string) {
return { id }
}
}
class UserRepo {
constructor(
private readonly logger: Logger,
private readonly database: Database
) {}
}
const container = new Container()
.registerClass('logger', Logger, [])
.registerClass('database', Database, [])
.registerClass('users', UserRepo, ['logger', 'database'])
const users = container.get('users')The two dependencies have different public shapes, so swapping them produces the error claimed by the example:
import { Container } from '@inferdi/inferdi'
class Logger {
info(message: string) {}
}
class Database {
findUser(id: string) {
return { id }
}
}
class UserRepo {
constructor(logger: Logger, database: Database) {}
}
new Container()
.registerClass('logger', Logger, [])
.registerClass('database', Database, [])
.registerClass('users', UserRepo, ['database', 'logger']) TypeScript uses structural typing. Two empty classes, or two classes with identical public members, are assignable to each other and cannot prove semantic order. Give contracts distinct shapes. If two values must remain different despite sharing a shape, brand the value types as described under Symbol Keys.
Key Uniqueness
Every fluent registration returns a container with a wider graph type. Registering an existing key again is an error:
import { Container } from '@inferdi/inferdi'
new Container()
.registerValue('dsn', 'postgres://localhost/app')
.registerValue('dsn', 'sqlite://memory') Use .override() when a test intentionally replaces a service. Keep the returned container from every registration; an older reference does not carry later graph state. Bad Practices shows the failure mode.
The uniqueness guard checks every possible value of a key type. After registering 'dsn', a candidate typed as 'dsn' | 'replica' is rejected because it might overwrite 'dsn' at runtime. Broad string and symbol keys remain valid when they cannot overlap the known graph, although widening a key also makes the graph less precise.
Dynamic Keys
Literal keys are checked directly by .get(). Narrow a key obtained at runtime with .has():
import { Container } from '@inferdi/inferdi'
const container = new Container()
.registerValue('answer', 42)
.registerAsyncFactory('name', async () => 'InferDI', [])
declare const key: string | symbol
if (container.has(key)) {
await container.getAsync(key)
}.has() proves registration. It does not prove that missing scope inputs are ready or that an async key is accepted by synchronous .get().
Lifetime in the Type
Each entry records its lifetime. A singleton cannot capture a scoped or transient dependency:
import { Container } from '@inferdi/inferdi'
class RequestContext {
readonly requestId = 'req-1'
}
class UserService {
constructor(readonly request: RequestContext) {}
}
new Container()
.registerClass('request', RequestContext, [], 'scoped')
.registerClass('users', UserService, ['request'], 'singleton') The default runtime contract repeats cycle and lifetime checks for casts, dynamic keys, and captured containers that TypeScript cannot inspect. { fast: true } is a separate fixed-graph contract with fewer runtime checks.
Readiness and Async Status
Scope inputs and declarative async dependencies also change which keys are ready and whether they resolve through .get() or .getAsync():
import { Container } from '@inferdi/inferdi'
type RequestContext = { requestId: string }
class Database {
query() {}
}
class Handler {
constructor(request: RequestContext, database: Database) {}
}
const root = new Container()
.declareScopeInputs<{ request: RequestContext }>()
.registerAsyncFactory('database', async () => new Database(), [])
.registerClass('handler', Handler, ['request', 'database'], 'scoped')
root.getAsync('handler')
const scope = root.createScope({ request: { requestId: 'req-1' } })
scope.get('handler')
const handler = await scope.getAsync('handler')The root lacks request, and the ready handler remains async because it depends on database. Continue with Scope Inputs and Async Dependencies.
