Instance Factory — a zero-dependency, metadata-driven Dependency Injection container for TypeScript.
Infact is intentionally decorator-agnostic: you supply a describeClass callback that reads metadata however you choose (Reflect.getMetadata, a manual registry, code generation, etc.). Infact handles singleton caching, scoped lifecycles, circular dependencies, provider overrides, and class substitution.
npm install @prostojs/infactimport { Infact, TInfactClassMeta } from '@prostojs/infact'
class Database {
query(sql: string) { return sql }
}
class UserRepo {
constructor(public db: Database) {}
}
// 1. Define metadata (however you like)
const meta: Record<string, TInfactClassMeta> = {
Database: { injectable: true, constructorParams: [] },
UserRepo: { injectable: true, constructorParams: [{ type: Database }] },
}
// 2. Create the container
const container = new Infact({
describeClass: (cls) => meta[cls.name],
})
// 3. Resolve
const repo = await container.get(UserRepo)
repo.db.query('SELECT 1') // works — Database was auto-createdEvery class is a singleton within its Infact instance by default. Calling get(UserRepo) twice returns the same object.
Infact does not read decorators or reflect metadata on its own. Instead, you provide a describeClass function that returns an TInfactClassMeta object for any given class constructor:
interface TInfactClassMeta {
injectable: boolean // must be true to allow instantiation
constructorParams: ParamMeta[] // describes each constructor argument
global?: boolean // share instance across all Infact containers
scopeId?: string | symbol // bind to a named scope
provide?: TProvideRegistry // override dependencies for this class subtree
properties?: (string | symbol)[] // instance properties to resolve after construction
}Each entry in constructorParams tells Infact how to resolve one constructor argument:
interface TInfactConstructorParamMeta {
type?: Function // the class to instantiate (or String/Number/etc.)
inject?: string | symbol // resolve from provide registry by token instead of type
circular?: () => Constructor // lazy ref for circular deps (type must be undefined)
nullable?: boolean // allow undefined when unresolvable
optional?: boolean // alias for nullable
label?: string // used in error messages
fromScope?: string | symbol // resolve this param from a specific scope
provide?: TProvideRegistry // extra provide overrides for this param subtree
}Infact maintains three levels of singleton registries, checked in this order:
| Tier | Lifetime | Created by |
|---|---|---|
| Scope | Until unregisterScope() |
registerScope(id) + get(Cls, { fromScope: id }) |
| Instance | Per Infact instance |
Default for all classes |
| Global | Cross-container (static) | global: true in class meta |
Creates a DI container. The full options interface:
interface TInfactOptions<Class, Prop, Param, Custom> {
// Required — returns class metadata
describeClass: (cls: Constructor) => TInfactClassMeta<Param> & Class
// Optional — returns metadata for a specific instance property
describeProp?: (cls: Constructor, key: string | symbol) => Prop
// Optional — custom resolver for constructor params
// Return a value to override default resolution, or undefined to fall through
resolveParam?: (opts: {
paramMeta, classMeta, classConstructor,
index, scopeId, customData,
instantiate: (cls) => Promise<instance>
}) => unknown | Promise<unknown>
// Optional — custom resolver for instance properties
resolveProp?: (opts: {
instance, key, initialValue, propMeta,
classMeta, classConstructor, scopeId, customData,
instantiate: (cls) => Promise<instance>
}) => unknown | Promise<unknown>
// Optional — store provide/replace context per instance (enables getForInstance)
storeProvideRegByInstance?: boolean
// Optional — lifecycle event listener
on?: (event: 'new-instance' | 'warn' | 'error', targetClass, message, args?) => void
}Resolves a class asynchronously. Returns a Promise<T>.
const instance = await container.get(MyClass)Options:
interface TInfactGetOptions {
provide?: TProvideRegistry // override providers for this resolution tree
replace?: TReplaceRegistry // substitute classes for this resolution tree
customData?: object // passed through to resolveParam / resolveProp
fromScope?: string | symbol // resolve from a named scope
hierarchy?: string[] // (internal) tracks resolution chain for error messages
}Resolves Class using the same provide/replace context that was used to create instance. Requires storeProvideRegByInstance: true.
const container = new Infact({
describeClass: (cls) => meta[cls.name],
storeProvideRegByInstance: true,
})
const parent = await container.get(Parent)
// child inherits Parent's provide/replace overrides
const child = await container.getForInstance(parent, ChildDep)Creates or destroys a named scope. Scoped instances are isolated from the main registry and from other scopes:
container.registerScope('request-1')
const a = await container.get(Service, { fromScope: 'request-1' })
const b = await container.get(Service, { fromScope: 'request-1' })
a === b // true — singleton within scope
container.unregisterScope('request-1') // all scoped instances are discardedResets the instance registry, instance-registry metadata, and all scopes. Useful for dev-mode hot reload.
Static method. Clears the global (cross-container) singleton registry. Use with care.
Builds a provide registry — a map of lazy factories keyed by class constructor or string token:
import { createProvideRegistry } from '@prostojs/infact'
const provide = createProvideRegistry(
[DatabaseConnection, () => new DatabaseConnection('postgres://...')],
['API_KEY', () => process.env.API_KEY],
)Providers are lazy — the factory runs once on first resolution and the result is cached.
Builds a replace registry — maps one class to another throughout a resolution tree:
import { createReplaceRegistry } from '@prostojs/infact'
const replace = createReplaceRegistry(
[ProductionMailer, MockMailer],
)
const service = await container.get(NotificationService, { replace })
// NotificationService depends on ProductionMailer,
// but MockMailer will be instantiated insteadAttach a provide registry to class metadata to override dependencies for that class and its entire subtree:
const meta = {
AppController: {
injectable: true,
constructorParams: [{ type: AuthService }],
provide: createProvideRegistry(
[AuthService, () => new AuthService('jwt-secret')],
),
},
AuthService: {
injectable: true,
constructorParams: [],
},
}You can also pass provide per-param to scope overrides to a single branch:
constructorParams: [
{
type: RepoA,
provide: createProvideRegistry(
[DbPool, () => new DbPool('read-replica')],
),
},
{ type: RepoB }, // uses default DbPool
]Or pass provide at resolution time:
await container.get(AppController, {
provide: createProvideRegistry(
[Logger, () => new ConsoleLogger()],
),
})Providers can be keyed by string token for non-class dependencies:
constructorParams: [
{ type: Object, inject: 'config', nullable: true },
]
// Somewhere upstream:
provide: createProvideRegistry(
['config', () => ({ port: 3000 })],
)Replace registries swap one class for another. The replacement class is instantiated using its own metadata:
const replace = createReplaceRegistry(
[OriginalService, MockService],
)
const instance = await container.get(OriginalService, { replace })
instance instanceof MockService // trueWhen two classes depend on each other, mark the circular param with a lazy circular function and set type to undefined:
// A depends on B, B depends on A
const meta = {
A: {
injectable: true,
constructorParams: [
{ type: undefined, circular: () => B },
],
},
B: {
injectable: true,
constructorParams: [
{ type: undefined, circular: () => A },
],
},
}Infact pre-creates a prototype-based shell object and fills it in after instantiation via Object.defineProperties, preserving non-enumerable properties.
Scopes provide isolated singleton registries — useful for per-request lifecycles in servers:
container.registerScope('request-42')
const userService = await container.get(UserService, {
fromScope: 'request-42',
})
// Later, discard all instances from that scope
container.unregisterScope('request-42')A class can also declare its scopeId in metadata, so it always resolves from that scope without passing fromScope at call site.
Note: global: true and scopeId cannot be combined — this throws an error.
Mark a class as global: true in its metadata to share a single instance across all Infact containers:
const meta = {
ConfigService: {
injectable: true,
global: true,
constructorParams: [],
},
}
const containerA = new Infact({ describeClass: (cls) => meta[cls.name] })
const containerB = new Infact({ describeClass: (cls) => meta[cls.name] })
const a = await containerA.get(ConfigService)
const b = await containerB.get(ConfigService)
a === b // trueInfact can resolve instance properties after construction. List property keys in properties and provide describeProp + resolveProp callbacks:
class MyService {
configValue?: string
computedProp: number = 0
}
const container = new Infact({
describeClass: () => ({
injectable: true,
constructorParams: [],
properties: ['configValue', 'computedProp'],
}),
describeProp: (cls, key) => {
// return property-level metadata
return { transform: (v: number) => v * 2 }
},
resolveProp: ({ key, initialValue, propMeta }) => {
if (key === 'configValue') return 'injected'
if (propMeta.transform) return propMeta.transform(initialValue)
},
})
const svc = await container.get(MyService)
svc.configValue // 'injected'
svc.computedProp // 0 (transform(0) = 0)The resolveParam callback lets you inject values that aren't class instances — environment variables, config objects, primitives:
const container = new Infact({
describeClass: (cls) => meta[cls.name],
resolveParam: ({ paramMeta, index }) => {
// Inject all String-typed params with a resolved value
if (paramMeta.type === String) {
return 'injected-string'
}
// Return undefined to fall through to default resolution
},
})The callback also receives an instantiate helper for manually triggering resolution of other classes within the current context:
resolveParam: async ({ paramMeta, instantiate }) => {
if (paramMeta.type === SomeAbstractClass) {
return instantiate(ConcreteImplementation)
}
}Monitor container activity via the on callback:
const container = new Infact({
describeClass: (cls) => meta[cls.name],
on(event, targetClass, message, args) {
if (event === 'error') console.error(`DI error in ${targetClass.name}: ${message}`)
if (event === 'warn') console.warn(`DI warning: ${message}`)
if (event === 'new-instance') console.log(`Created ${targetClass.name}`)
},
})import type {
TInfactOptions,
TInfactClassMeta,
TInfactConstructorParamMeta,
TInfactGetOptions,
TProvideRegistry,
TReplaceRegistry,
TProvideFn,
} from '@prostojs/infact'MIT