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

@dmytromykhailiuk/injectable

v1.0.0

Published

Dependency injection with nested containers, full TypeScript typings, and ergonomic API

Downloads

88

Readme

@dmytromykhailiuk/injectable

Tiny, zero-dependency DI container for TypeScript with nested scopes.

If you've used providers in Angular or NestJS and wished for the same model without the framework, this package gives you exactly that: hierarchical containers, multi-providers, and typed injection tokens — in a single file, with no decorators and no reflect-metadata.

Why this library

  • Zero dependencies. One file, dual CJS/ESM build, ships only dist/.
  • Hierarchical containers. Parent / child resolution with host, skipSelf, and optional flags — model request scopes, test overrides, or feature modules without ceremony.
  • Strict TypeScript. Generic inject<T>() / get<T>(), typed injection tokens, no any in the public surface.

Installation

npm i @dmytromykhailiuk/injectable

Requires Node ≥ 18. No reflect-metadata, no experimentalDecorators needed in the consuming project.

Quick start

import {
  createContainer,
  inject,
  createInjectionToken,
} from "@dmytromykhailiuk/injectable";

class Logger {
  log(msg: string) {
    console.log(`[log] ${msg}`);
  }
}

class UserService {
  private logger = inject(Logger);

  greet(name: string) {
    this.logger.log(`hello, ${name}`);
  }
}

const container = createContainer();
container.register(UserService, Logger);

container.get(UserService).greet("world");

inject() is only valid while a provider is being instantiated. Use container.get() to pull instances out from the outside.

Providers

Anything you pass to container.register() is a provider. The shapes are:

Class provider

The simplest form — pass the class. The container will new it on first registration and cache the instance.

class Mailer {}

container.register(Mailer);
container.get(Mailer); // same instance every time

useValue — bind a static value

const API_URL = createInjectionToken("API_URL");

container.register({
  provide: API_URL,
  useValue: "https://api.example.com",
});

container.get<string>(API_URL); // "https://api.example.com"

useCreate — factory or class

useCreate accepts a zero-arg factory or a class. Use inject() inside the factory to pull in other providers — that's how you wire constructor arguments.

interface Logger {
  // logger interface
}

const LOGGER = createInjectionToken("logger");

function loggerResolver() {
  const platform = inject(PlatformFacade);
  const browserLogger = inject(BrowserLogger);
  const serverLogger = inject(ServerLogger);

  return platform.isServer ? serverLogger : browserLogger;
}

container.register({
  provide: LOGGER,
  useCreate: loggerResolver,
});

You can define class or function with inject argments:

function loggerResolver(
  platform = inject(PlatformFacade),
  browserLogger = inject(BrowserLogger),
  serverLogger = inject(ServerLogger)
) {
  return platform.isServer ? serverLogger : browserLogger;
}

class Group {
  constructor(
    private logger = inject<Logger>(LOGGER);
  ) {}
}

useExisting — alias

useExisting resolves the target immediately and stores the same instance under the new token.

container.register(Logger);
container.register({ provide: "AppLogger", useExisting: Logger });

container.get("AppLogger") === container.get(Logger); // true

multi: true — collect into an array

Register the same token several times with multi: true and you get an array back.

const HOOKS = createInjectionToken("Hooks");

container.register(
  { provide: HOOKS, useValue: "before", multi: true },
  { provide: HOOKS, useValue: "after", multi: true }
);

container.get<string[]>(HOOKS, { multi: true }); // ["before", "after"]

For class-based multi providers there is an array sugar — [Class] — that both inject() and get() accept:

class Middleware {}

container.register({ provide: Middleware, useCreate: Cors, multi: true });
container.register({ provide: Middleware, useCreate: Auth, multi: true });

container.get([Middleware]); // Middleware[]
inject([Middleware]); // same, inside a factory

Injection tokens

For anything that isn't a class (primitives, interfaces, abstract contracts), create a symbol-based token. Symbols are identity-based, so they never collide with strings used elsewhere.

import { createInjectionToken } from "@dmytromykhailiuk/injectable";

const CONFIG = createInjectionToken("CONFIG");

container.register({ provide: CONFIG, useValue: { retries: 3 } });

interface Config {
  retries: number;
}
container.get<Config>(CONFIG).retries; // 3

Plain strings work too (container.get("CONFIG")), but symbols are recommended for anything beyond a quick prototype.

inject() vs container.get()

Both resolve providers, but they're used in different places:

| | inject(token, opts?) | container.get(token, opts?) | | -------------------- | ---------------------------------------------------------------------- | ----------------------------------------- | | Called from | inside a class constructor or useCreate factory, during registration | application code that holds the container | | Container | uses the active resolver implicitly | uses the container you call it on | | Outside registration | throws "Inject must be used in scope of Injectable entity!" | always valid |

class OrderService {
  private mailer = inject(Mailer); // OK — inside constructor
}

inject(Mailer); // throws — no active container
container.get(Mailer); // OK

Injection options

Both inject() and container.get() accept the same options:

interface InjectOptions {
  host?: boolean; // resolve only from this container, ignore parents
  skipSelf?: boolean; // skip this container, resolve from parent only
  multi?: boolean; // treat the result as an array
  optional?: boolean; // return undefined instead of throwing when missing
}
const analytics = inject(Analytics, { optional: true }) ?? new NoopAnalytics();

container.get(Logger, { skipSelf: true }); // explicitly use the parent's Logger
container.get(Logger, { host: true }); // only this container's Logger

Nested containers

Pass a parent to createContainer and you get hierarchical resolution: the child checks itself first, then falls back to the parent.

const root = createContainer();
root.register({ provide: "API_URL", useValue: "https://prod.example.com" });
root.register(Logger);

const test = createContainer(root);
test.register({ provide: "API_URL", useValue: "http://localhost:3000" });

test.get("API_URL"); // "http://localhost:3000" — child wins
test.get(Logger); // inherited from root

For multi providers the arrays merge — child values come first, then parent's:

const root = createContainer();
root.register({ provide: HOOKS, useValue: "root", multi: true });

const child = createContainer(root);
child.register({ provide: HOOKS, useValue: "child", multi: true });

child.get<string[]>(HOOKS, { multi: true }); // ["child", "root"]

Deferred registration

Registration order doesn't matter. If a factory calls inject() for a token that hasn't been registered yet, the container parks the registration and re-runs it as soon as the missing token arrives.

class Db {}
class UserService {
  private db = inject(Db);
}

const c = createContainer();
c.register(UserService); // Db not registered yet — parked, not thrown
c.register(Db); // triggers UserService registration automatically

c.get(UserService); // ready

This also works across parent / child boundaries: a child waits for a token a parent will register.

Lifecycle: destroy() and subscribe()

destroy() clears all instances, drops subscribers and pending registrations, and emits a container-destroyed event. Use it for per-request child containers or test teardown.

subscribe() notifies you when providers register and when the container is destroyed. The returned function unsubscribes.

const container = createContainer();

const unsubscribe = container.subscribe((event) => {
  if (event.type === "provider-registered") {
    console.log("registered:", event.token.toString());
  } else {
    console.log("container destroyed");
  }
});

container.register(Logger);
// ...
unsubscribe();
container.destroy();

Patterns

Per-request scope

function handleRequest(req: Request) {
  const scope = createContainer(rootContainer);
  scope.register({ provide: "REQ", useValue: req });

  try {
    return scope.get(RequestHandler).run();
  } finally {
    scope.destroy();
  }
}

Swap a real service in tests

const test = createContainer(appContainer);
test.register({ provide: Mailer, useValue: new FakeMailer() });

expect(test.get(OrderService).checkout()).toMatchSnapshot();

Group registrations as a "module"

export function registerAuthModule(c: Container) {
  c.register(PasswordHasher, TokenIssuer, {
    provide: AuthService,
    useCreate: () =>
      new AuthService(inject(TokenIssuer), inject(PasswordHasher)),
  });
}

registerAuthModule(container);

API reference

// Tokens & containers
createInjectionToken(id: string): symbol;
createContainer(parent?: Container): Container;

// Container
interface Container {
  register(...providers: ProviderOption[]): void;
  get<T>(provider: symbol | string | (new () => T), options?: InjectOptions): T;
  get<T>(provider: [new () => T], options?: InjectOptions): T[];
  destroy(): void;
  subscribe(fn: (event: ContainerEvent) => void): () => void;
}

// Provider shapes
type ProviderOption<T = unknown> =
  | (new () => T)
  | { provide: Provider; useValue: T;                              multi?: boolean }
  | { provide: Provider; useExisting: Provider<T>;                 multi?: boolean }
  | { provide: Provider; useCreate: (new () => T) | (() => T);     multi?: boolean };

type Provider<T = unknown> = symbol | string | (new () => T);

// Injection
inject<T>(provider: symbol | string | (new () => T), options?: InjectOptions): T;
inject<T>(provider: [new () => T], options?: InjectOptions): T[];

interface InjectOptions {
  host?: boolean;
  skipSelf?: boolean;
  multi?: boolean;
  optional?: boolean;
}

// Events
type ContainerEvent =
  | { type: "provider-registered"; token: string | symbol }
  | { type: "container-destroyed" };

Errors

  • Inject must be used in scope of Injectable entity!inject() was called outside a register() factory or constructor.
  • Provider "<token>" already registered! — same non-multi token registered twice in the same container.
  • Provider "<token>" is not multi! — same token was registered both as multi and non-multi.

Limitations

  • Synchronous resolutionuseCreate cannot return a Promise. Resolve async work upfront (or behind a lazy method) and register the result.
  • No decorators. @Injectable / @Inject aren't part of this package by design. All wiring is explicit via inject().
  • No built-in module system. Group providers with a plain function (see Patterns above).

License

MIT © Dmytro Mykhailiuk