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

@ironbase/di

v1.0.0

Published

A small, type-safe dependency container for TypeScript

Readme

@ironbase/di

A small, type-safe dependency container for TypeScript with singleton, scoped, and transient lifetimes.

@ironbase/di provides explicit dependency management without decorators, reflection metadata, constructor inspection, or a global container. Its core API is set, get, and createScope.

Installation

bun add @ironbase/di
npm install @ironbase/di

Quick Start

Declare a service map, create a container, and register typed factories:

import { createContainer } from "@ironbase/di";

class Logger {
  info(message: string) {
    console.log(message);
  }
}

class Database {
  constructor(private readonly logger: Logger) {}

  async [Symbol.asyncDispose]() {
    this.logger.info("Database connection closed");
  }
}

class UserRepository {
  constructor(private readonly database: Database) {}
}

interface Services {
  logger: Logger;
  database: Database;
  users: UserRepository;
}

const container = createContainer<Services>();

container
  .set("logger", "singleton", () => new Logger())
  .set("database", "scoped", ({ get }) => new Database(get("logger")))
  .set("users", "transient", ({ get }) => new UserRepository(get("database")));

await using scope = container.createScope();

const users = scope.get("users"); // UserRepository

The service map connects each key to its value type. TypeScript rejects unknown keys and factories that return the wrong type.

Examples

The examples directory contains independently runnable examples:

  • basic.ts demonstrates typed registration and resolution.
  • scopes.ts isolates scoped values across concurrent requests.
  • disposal.ts demonstrates native reverse-order cleanup.
  • errors.ts prints scope and circular dependency diagnostics.
  • circular-dependencies.ts detects a cycle and demonstrates how to refactor it into an acyclic graph.

Run an example with Bun:

bun examples/scopes.ts

Lifetimes

| Lifetime | Creation | Reuse | Owner | | --- | --- | --- | --- | | singleton | On first resolution | Every resolution | Root container | | scoped | On first resolution within a scope | Within that scope | Child scope | | transient | On every resolution | Never | Resolver that created it |

Factories are lazy and synchronous. Asynchronous resource cleanup is supported, but asynchronous factories are not part of the initial API.

Singleton

A singleton is created once and stored by the root container. Its factory always receives the root resolver, even when it is first requested through a child scope. This prevents a singleton from capturing a scoped dependency.

Transient dependencies created by a singleton factory are also owned by the root container.

Scoped

A scoped dependency is created once per child scope. Resolving one directly from the root throws an error instead of creating an implicit root scope.

container.get("database");
// Throws: Cannot resolve scoped dependency "database" outside a scope

Transient

A transient dependency is created for every get call and every time another factory requests it.

Scopes

The root can create multiple independent scopes, making the container safe for concurrent request handling:

async function handleRequest() {
  await using scope = container.createScope();

  const first = scope.get("database");
  const second = scope.get("database");

  console.log(first === second); // true
}

await Promise.all([handleRequest(), handleRequest()]);

Each request receives a different database instance. Singleton instances are shared by all scopes.

Nested scopes are intentionally unsupported. Calling createScope() on a scope throws. This keeps ownership and disposal boundaries unambiguous.

Use isScope() when code needs to distinguish a root container from a scope:

container.isScope(); // false

const scope = container.createScope();
scope.isScope(); // true

await scope.dispose();

Disposal

Containers and scopes implement asynchronous disposal. Created values are disposed in reverse creation order when they define either of the native disposal methods:

class Connection {
  async [Symbol.asyncDispose]() {
    await this.close();
  }

  private async close() {
    // Close the underlying resource.
  }
}

Symbol.asyncDispose takes precedence when a value implements both protocols. Calls to dispose() are idempotent.

const scope = container.createScope();

try {
  scope.get("database");
} finally {
  await scope.dispose();
}

Singletons and root-created transients are disposed with the root. Scoped and scope-created transient values are disposed with their scope. Dispose every scope before disposing the root container.

Resolution Errors

Resolution errors include the dependency path to make configuration failures actionable:

Cannot resolve "database": missing dependency "logger"
Resolution path: users -> database -> logger

Circular dependencies are detected before a factory is invoked again:

Circular dependency detected: users -> database -> users

The container throws when:

  • a key is registered more than once;
  • an unknown key is resolved;
  • a scoped dependency is resolved from the root;
  • createScope() is called on a scope;
  • a circular dependency is detected; or
  • a disposed container or scope is used.

New unique registrations may be added to the root at any time. Registrations are not locked after the first resolution or scope creation.

API

createContainer<Services>()

Creates an isolated root container for a service map.

const container = createContainer<Services>();

set(key, type, factory)

Registers a lazy factory. The key, lifetime discriminator, factory resolver, and factory return value are type checked.

container.set("logger", "singleton", () => new Logger());

The lifetime discriminator is one of:

type Lifetime = "singleton" | "scoped" | "transient";

Registrations are represented as a discriminated union:

interface Resolver<Services> {
  get<Key extends keyof Services>(key: Key): Services[Key];
  isScope(): boolean;
}

type Factory<Services, Value> = (resolver: Resolver<Services>) => Value;

type Registration<Services, Value> =
  | { type: "singleton"; factory: Factory<Services, Value> }
  | { type: "scoped"; factory: Factory<Services, Value> }
  | { type: "transient"; factory: Factory<Services, Value> };

set returns the container so registrations can be chained.

get(key)

Resolves a dependency and infers its result from the service map.

const logger = container.get("logger"); // Logger

Factories receive a restricted resolver exposing typed get and isScope operations, not registration or scope creation.

createScope()

Creates an independent child scope. The root may create any number of scopes. A scope cannot create another scope.

isScope()

Returns false for a root container and true for a child scope.

dispose()

Disposes values owned by the resolver in reverse creation order and returns a Promise<void>. Repeated calls are safe.

The container and scopes also implement [Symbol.asyncDispose]() for use with await using.

Comparison

The comparison below focuses on public behavior relevant to this package. It is based on each project's documentation as of August 29, 2026.

| Feature | @ironbase/di | Awilix | TSyringe | InversifyJS | TypeDI | | --- | --- | --- | --- | --- | --- | | Singleton lifetime | Yes | Yes | Yes | Yes | Yes | | Scoped lifetime | Explicit child scope | Explicit child scope | Container-scoped | Resolution-request scope | Named container | | Transient lifetime | Yes | Yes | Yes | Yes | Yes | | Typed string lookup | Service map | Typed cradle | Manual generic | Manual generic | Prefer Token<T> | | Decorator-free registration | Yes | Yes | Provider API available | Binding API available | Container API available | | Reflection metadata required | No | No | For decorator usage | For metadata injection | For decorator usage | | Nested scopes | No | Yes | Yes | Container hierarchy | Multiple containers | | Native disposal symbols | Yes | No | No | No | No | | Async dependency graph | No | No | No | Yes | No |

The goal is not to cover every DI pattern. @ironbase/di focuses on explicit factories, strict scope boundaries, native cleanup, and a small API.

Non-Goals

The initial package intentionally does not include:

  • decorators or reflection metadata;
  • automatic constructor inspection;
  • a global default container;
  • nested scopes or child registration overrides;
  • asynchronous factories;
  • module scanning or automatic registration;
  • activation hooks or interception; or
  • multiple registrations for one key.

These constraints keep dependency construction visible and the implementation small.