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

never-di

v0.0.21

Published

A lightweight, immutable, dependency-free, function-only dependency injection (DI) container for TypeScript.

Readme

never-di

A lightweight, dependency-free, function-only dependency injection (DI) container for TypeScript.
No decorators, no reflection, no classes — just plain functions with strong type safety.


Design

never-di is intentionally minimal and built with these principles:

  • Function-only: factories are plain functions. No classes, no decorators.
  • Compile-time type safety: dependency errors are caught at compile time, not runtime.
  • Immutable builder: every builder step returns a new draft; the receiver is never mutated, so branching a draft never leaks state across branches.
  • Multi-binding: register multiple factories for the same token and resolve an array. The element type is the inferred common shape of the factories' returns — no declaration needed.
  • Lazy dependencies: explicitly mark tokens as lazy, and they resolve to memoized thunks.
  • Singletons by design: factories run once and results are cached.
  • Cycle-safe: eager dependency cycles fail with a clear circular dependency detected error instead of a stack overflow.
  • Lightweight: small, simple, no external dependencies.

Scope Model

There is exactly one scope: singleton.

  • Each token is resolved once per container.
  • Factories run only on first resolution, then results are cached.
  • Similar to ESM modules: load once, reuse everywhere.

If you need per-request behavior, create a new container instance.
If you need transient behavior, put it inside your factory.


API

createContainerDraft()

Start building a container draft:

import { createContainerDraft } from "never-di";

The draft exposes:

  • assign(factory) – assign a single factory to its token.
  • assignMany([factories]) – assign multiple factories for the same token (multi-binding).
  • defineLazy(factory) – mark a factory’s token as lazy; the container enforces that it must be assigned before sealing. Intended to allow direct cyclic dependencies.
  • seal() – finalize the container; returns a runtime Container with resolve and bind.

Builder stages

The draft is a two-stage builder:

  1. Stage 1 (a fresh draft, or after defineLazy) exposes defineLazy, assign, and assignMany.
  2. Stage 2 (after the first assign/assignMany) additionally exposes seal.

seal() is therefore unavailable until at least one factory has been assigned — an empty draft cannot be sealed.

Factory metadata

Factories carry two optional metadata fields:

  • factory.token – the unique key the factory is registered under. Required for assign, assignMany, and defineLazy.
  • factory.dependsOn – an ordered, as const tuple of dependency tokens, mapped positionally onto the factory's parameters.

A factory passed to bind is a one-off procedure and does not need a token — it is executed against the container but never registered.


Usage

Basics

import { createContainerDraft } from "never-di";

foo.token = "foo" as const;
function foo(): number {
  return 1;
}

bar.token = "bar" as const;
bar.dependsOn = ["foo"] as const;
function bar(foo: number): string {
  return `bar(${foo})`;
}

const container = createContainerDraft()
  .assign(foo)
  .assign(bar)
  .seal();

console.log(container.resolve("bar")); // "bar(1)"

Multi-binding

import { createContainerDraft } from "never-di";

h1.token = "handler" as const;
function h1(): string { return "h1"; }

h2.token = "handler" as const;
function h2(): string { return "h2"; }

const container = createContainerDraft()
  .assignMany([h1, h2])
  .seal();

console.log(container.resolve("handler")); // ["h1", "h2"]

Lazy dependencies ( breaking cycles )

import { createContainerDraft } from "never-di";

Foo.token = "Foo" as const;
Foo.dependsOn = ["Bar"] as const;
function Foo(b: { bar: string }) {
  return { foo: "foo->" + b.bar };
}

Bar.token = "Bar" as const;
Bar.dependsOn = ["Foo"] as const;
function Bar(foo: () => { foo: string }) {
  return { bar: "bar->" + foo().foo };
}

const c = createContainerDraft()
  .defineLazy(Foo)
  .assign(Foo)
  .assign(Bar)
  .seal();

const bar = c.resolve("Bar");

console.log(bar.bar);

The bind method ( usually for side-effects )

import { createContainerDraft } from "never-di";

Foo.token = "Foo" as const;
Foo.dependsOn = ["Bar"] as const;
function Foo(b: { bar: string }) {
  return { foo: "foo->" + b.bar };
}

Bar.dependsOn = ["Foo"] as const;
function Bar(foo: () => { foo: string }) {
  console.log({ bar: "bar->" + foo().foo });
}

const c = createContainerDraft()
  .defineLazy(Foo)
  .assign(Foo)
  .seal();

const bar = c.bind(Bar);

bar();

Compile-time guarantees (with examples)

1) Only depend on already-registered tokens

✅ good

Foo.token = "Foo" as const;
function Foo() { return 123; }

Bar.token = "Bar" as const;
Bar.dependsOn = ["Foo"] as const;
function Bar(foo: number) { return foo.toString(); }

createContainerDraft()
  .assign(Foo)
  .assign(Bar)
  .seal();

❌ type error (unknown token)

Bar.token = "Bar" as const;
Bar.dependsOn = ["Missing"] as const;
function Bar(x: unknown) { return x; }

createContainerDraft()
  .assign(Bar);
// Error: Bar.dependsOn includes "Missing", which is not in the draft registry.

2) Parameter types must match dependency token types

✅ good

Foo.token = "Foo" as const;
function Foo() { return 123; }

Bar.token = "Bar" as const;
Bar.dependsOn = ["Foo"] as const;
function Bar(foo: number) { return foo.toFixed(2); }

createContainerDraft().assign(Foo).assign(Bar).seal();

❌ type error

Foo.token = "Foo" as const;
function Foo() { return 123; }

Bar.token = "Bar" as const;
Bar.dependsOn = ["Foo"] as const;
function Bar(foo: string) {
  return foo.toUpperCase();
}
// Error: Parameter 1 of Bar must be the type produced by token "Foo" (number).

3) Dependency order is enforced

✅ good

A.token = "A" as const;
function A() { return { a: 1 }; }

B.token = "B" as const;
function B() { return { b: 2 }; }

C.token = "C" as const;
C.dependsOn = ["A", "B"] as const;
function C(a: { a: number }, b: { b: number }) {
  return [a.a, b.b];
}

createContainerDraft().assign(A).assign(B).assign(C).seal();

❌ type error

C.token = "C" as const;
C.dependsOn = ["A", "B"] as const;
function C(b: { b: number }, a: { a: number }) {
  return [a.a, b.b];
}
// Error: Param1 must match "A", Param2 must match "B"

4) Lazy tokens resolve to thunks

✅ good

Foo.token = "Foo" as const;
function Foo() { return { foo: 42 }; }

Bar.token = "Bar" as const;
Bar.dependsOn = ["Foo"] as const;
function Bar(foo: () => { foo: number }) {
  return foo().foo + 1;
}

createContainerDraft()
  .defineLazy(Foo)
  .assign(Foo)
  .assign(Bar)
  .seal();

❌ type error

Bar.token = "Bar" as const;
Bar.dependsOn = ["Foo"] as const;
function Bar(foo: { foo: number }) {
  return foo.foo + 1;
}
// Error: Token "Foo" is lazy; dependents must accept () => { foo: number }

5) Multi-bind infers the factories' common shape

A token bound by several factories resolves to an array. There is nothing to declare — the container infers the element type from the factories' return types and computes their common shape: the members present in every return, each typed as the union of that member's types across the factories.

The common shape is a single flat object regardless of how many factories are bound, so it stays cheap and readable (no growing union to wade through in your editor). The coherence rule falls out of the same computation:

  • All factories returning objects that share ≥1 member → the common shape of those members. The canonical use case: distinct command/handler shapes sharing an interface or discriminant.
  • All factories returning the same primitive → that primitive (e.g. string[]).
  • Objects that share no member, differing primitives, or a mix of objects and primitives → rejected as incoherent.

Note: because the element type is the flat common shape (not a union of the concrete return types), resolve results expose only the shared members and do not narrow on a discriminant. Consume them through the shared interface.

✅ good — heterogeneous command shapes sharing { name, handler } (no declaration)

interface GetValue { name: "get_value"; handler: () => void; getOnly: number }
interface PutValue { name: "put_value"; handler: () => void; putOnly: boolean }

GetCmd.token = "Command" as const;
function GetCmd(): GetValue { return { name: "get_value", handler() {}, getOnly: 1 }; }

PutCmd.token = "Command" as const;
function PutCmd(): PutValue { return { name: "put_value", handler() {}, putOnly: true }; }

const c = createContainerDraft().assignMany([GetCmd, PutCmd]).seal();
const commands = c.resolve("Command"); // { name: string; handler: () => void }[]
commands.forEach((cmd) => cmd.handler()); // shared members only (no getOnly/putOnly)

A consumer can depend on the token and type its parameter as the shared interface (or anything wider) — the resolved array is assignable to it:

interface Command { name: string; handler: () => void }

KvService.token = "Service" as const;
KvService.dependsOn = ["Command"] as const;
function KvService(commands: Command[]) { /* ... */ }

createContainerDraft().assignMany([GetCmd, PutCmd]).assign(KvService).seal();

✅ good — identical primitive returns

H1.token = "Handler" as const; function H1() { return "h1"; }
H2.token = "Handler" as const; function H2() { return "h2"; }

createContainerDraft().assignMany([H1, H2]).seal(); // string[]

❌ type error — returns share no common members

H1.token = "Handler" as const; function H1() { return { a: 1 }; }
H2.token = "Handler" as const; function H2() { return { b: 2 }; }

createContainerDraft()
  .assignMany([H1, H2])
  .seal();
// Error: assignMany factory return types share no common members
//        (seal does not exist on the returned error type)

6) resolve(token) is token-safe and value-typed

✅ good

Foo.token = "Foo" as const;
function Foo() { return 123; }

const c = createContainerDraft().assign(Foo).seal();
const x = c.resolve("Foo");   // number
x.toFixed(2);

❌ type error

Foo.token = "Foo" as const;
function Foo() { return 123; }

const c = createContainerDraft().assign(Foo).seal();
c.resolve("Nope");
// Error: Argument of type '"Nope"' is not assignable to parameter of type '"Foo"'

Note: an empty draft cannot be sealed — seal() only becomes available after the first assign/assignMany. createContainerDraft().seal() is a compile-time error (Property 'seal' does not exist).


7) Predeclare tokens with defineLazy and wire later

✅ good

A.token = "A" as const;
A.dependsOn = ["B"] as const;
function A(b: () => string) { return "A->" + b(); }

B.token = "B" as const;
B.dependsOn = ["A"] as const;
function B(a: () => string) { return "B->" + a(); }

const c = createContainerDraft()
  .defineLazy(A)
  .assign(A)
  .assign(B)
  .seal();

c.resolve("A");

8) bind(fn) checks the dependency signature too

✅ good

Svc.token = "Svc" as const;
function Svc() { return { ping: () => "pong" }; }

Runner.dependsOn = ["Svc"] as const;
function Runner(svc: { ping: () => string }) {
  return () => console.log(svc.ping());
}

const c = createContainerDraft().assign(Svc).seal();
c.bind(Runner)(); // prints "pong"

❌ type error

Runner.dependsOn = ["Svc"] as const;
function Runner(svc: { nope: () => void }) { }
// Error: Parameter for "Svc" must match its produced type

9) Bonus: end-to-end misuse is caught

❌ type error

Foo.token = "Foo" as const;
function Foo() { return 123; }

const c = createContainerDraft().assign(Foo).seal();
const v = c.resolve("Foo"); // number
v.toUpperCase();
// Error: 'toUpperCase' does not exist on type 'number'

Notes

  • No class support, no decorators, no custom scopes — intentionally out of scope.
  • Contributions are welcome for:
    • Bug reports
    • Unit tests
    • Type improvements

Inspired by typed-inject, but with a simpler, function-only design.