npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@prostojs/infact

v0.4.1

Published

Instance Factory and Instance Registry for Metadata based Dependency Injection scenarios

Readme

@prostojs/infact

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.

Install

npm install @prostojs/infact

Quick Start

import { 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-created

Every class is a singleton within its Infact instance by default. Calling get(UserRepo) twice returns the same object.

Core Concepts

Metadata via describeClass

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
}

Constructor Parameters

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
}

Singleton Tiers

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 |

API

new Infact(options)

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
}

container.get(Class, opts?, optional?)

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
}

container.getForInstance(instance, Class, opts?)

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)

container.registerScope(scopeId) / container.unregisterScope(scopeId)

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 discarded

container._cleanup()

Resets the instance registry, instance-registry metadata, and all scopes. Useful for dev-mode hot reload.

Infact._cleanupGlobal()

Static method. Clears the global (cross-container) singleton registry. Use with care.

createProvideRegistry(...entries)

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.

createReplaceRegistry(...entries)

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 instead

Features

Provide Overrides

Attach 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 (Class Substitution)

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 // true

Circular Dependencies

When 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.

Scoped Instances

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.

Global Instances

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 // true

Property Resolution

Infact 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)

Custom Param Resolution

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)
    }
}

Event Listener

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}`)
    },
})

Exported Types

import type {
    TInfactOptions,
    TInfactClassMeta,
    TInfactConstructorParamMeta,
    TInfactGetOptions,
    TProvideRegistry,
    TReplaceRegistry,
    TProvideFn,
} from '@prostojs/infact'

License

MIT