{"_id":"@dmytromykhailiuk/injectable","_rev":"3-9934fb6ada217b00bc0127f4f07ec565","name":"@dmytromykhailiuk/injectable","dist-tags":{"latest":"1.0.2"},"versions":{"1.0.0":{"name":"@dmytromykhailiuk/injectable","version":"1.0.0","keywords":["dependency-injection","di","ioc","container","injectable","inject","provider","nested-container","hierarchical","multi-provider","scope","singleton","factory","service-locator","typescript","ts","zero-dependencies","no-decorators","esm","tree-shakeable","nestjs","angular"],"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","_id":"@dmytromykhailiuk/injectable@1.0.0","maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"homepage":"https://github.com/dmytromykhailiuk/injectable#readme","bugs":{"url":"https://github.com/dmytromykhailiuk/injectable/issues"},"dist":{"shasum":"8f1b18a11c0b07e11917e60273d32f7cbdb3e369","tarball":"https://registry.npmjs.org/@dmytromykhailiuk/injectable/-/injectable-1.0.0.tgz","fileCount":9,"integrity":"sha512-C+siWvDKlPj5bzo5KkTBghTNNw/komKW2Mns9BO6H2JKjk2sP8CYhknSpbKKDQQe7z2UbyxCaOgDfjbvS+ZzXQ==","signatures":[{"sig":"MEUCIQCYtOmj017Sly/iFvD8/cWefZu2fjslhK9gO6Ui9n9d8AIgfs3uYJbBnRGMBTT6NbSaQ/esWb935fdCCIYTGtiOILA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":62883},"main":"./dist/index.js","types":"./dist/index.d.ts","module":"./dist/index.mjs","engines":{"node":">=18.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"}},"gitHead":"cbb90ea63dc884bfb2c339a1047368ac71b2ba52","scripts":{"dev":"tsup --watch","lint":"eslint src","build":"tsup","typecheck":"tsc --noEmit","prepublishOnly":"npm run typecheck && npm run build"},"_npmUser":{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"},"repository":{"url":"git+https://github.com/dmytromykhailiuk/injectable.git","type":"git"},"_npmVersion":"11.6.2","description":"Dependency injection with nested containers, full TypeScript typings, and ergonomic API","directories":{},"_nodeVersion":"24.12.0","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","eslint":"^9.0.0","typescript":"^5.5.0","typescript-eslint":"^8.60.1"},"_npmOperationalInternal":{"tmp":"tmp/injectable_1.0.0_1780920695700_0.3579813232315703","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@dmytromykhailiuk/injectable","version":"1.0.1","keywords":["dependency-injection","di","ioc","container","injectable","inject","provider","nested-container","hierarchical","multi-provider","scope","singleton","factory","service-locator","typescript","ts","zero-dependencies","no-decorators","esm","tree-shakeable","nestjs","angular"],"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","_id":"@dmytromykhailiuk/injectable@1.0.1","maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"homepage":"https://github.com/dmytromykhailiuk/injectable#readme","bugs":{"url":"https://github.com/dmytromykhailiuk/injectable/issues"},"dist":{"shasum":"c1de12c76d53bcf0612005909bb3c43f57f05a0e","tarball":"https://registry.npmjs.org/@dmytromykhailiuk/injectable/-/injectable-1.0.1.tgz","fileCount":9,"integrity":"sha512-NNCvzRvVyt5s3hEy50z6gjrC+WFYfxqP8e8BokJy8xWWJFyZR07+N1yfIMGgyQENBrID4wNooKQDfX6nzdhemQ==","signatures":[{"sig":"MEYCIQCReOSAQ4mLxpgoupWvLDi+0Rs+PtBLnI5FMf4F2iZ0mQIhAOuguMDRPW0as1aNjLeJCUWdXBkhSaRYpkJ86GTMJAY0","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":106660},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"gitHead":"0cce6fc84dd30a7ce228fbe64b25738c7442e285","scripts":{"dev":"tsup --watch","lint":"biome check .","test":"vitest run","build":"tsup","format":"biome format --write .","verify":"npm run lint && npm run typecheck && npm run test && npm run build","lint:fix":"biome check --write .","typecheck":"tsc --noEmit","playground":"tsx playground/index.ts","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"npm run build","playground:watch":"tsx watch playground/index.ts"},"_npmUser":{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"},"repository":{"url":"git+https://github.com/dmytromykhailiuk/injectable.git","type":"git"},"_npmVersion":"11.6.2","description":"Dependency injection with nested containers, full TypeScript typings, and an ergonomic, decorator-free API. Angular / NestJS providers without the framework.","directories":{},"sideEffects":false,"_nodeVersion":"24.12.0","_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.23.1","tsup":"^8.3.5","vitest":"^2.1.8","typescript":"^5.7.3","@types/node":"^22.10.5","@biomejs/biome":"^1.9.4","@vitest/coverage-v8":"^2.1.8"},"_npmOperationalInternal":{"tmp":"tmp/injectable_1.0.1_1784714888013_0.8780525419945673","host":"s3://npm-registry-packages-npm-production"}},"1.0.2":{"name":"@dmytromykhailiuk/injectable","version":"1.0.2","description":"Dependency injection with nested containers, full TypeScript typings, and an ergonomic, decorator-free API. Angular / NestJS providers without the framework.","type":"module","sideEffects":false,"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","keywords":["dependency-injection","di","ioc","container","injectable","inject","provider","nested-container","hierarchical","multi-provider","scope","singleton","factory","service-locator","typescript","ts","zero-dependencies","no-decorators","esm","tree-shakeable","nestjs","angular"],"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"scripts":{"build":"tsup","dev":"tsup --watch","playground":"tsx playground/index.ts","playground:watch":"tsx watch playground/index.ts","typecheck":"tsc --noEmit","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","lint":"biome check .","lint:fix":"biome check --write .","format":"biome format --write .","verify":"npm run lint && npm run typecheck && npm run test && npm run build","prepublishOnly":"npm run build"},"engines":{"node":">=18"},"devDependencies":{"@biomejs/biome":"^1.9.4","@types/node":"^22.10.5","@vitest/coverage-v8":"^2.1.8","tsup":"^8.3.5","tsx":"^4.23.1","typescript":"^5.7.3","vitest":"^2.1.8"},"repository":{"type":"git","url":"git+https://github.com/dmytromykhailiuk/injectable.git"},"bugs":{"url":"https://github.com/dmytromykhailiuk/injectable/issues"},"homepage":"https://dmytromykhailiuk.github.io/injectable/","gitHead":"a311645441e990d79e68de274c9cb18a11c5a4ce","_id":"@dmytromykhailiuk/injectable@1.0.2","_nodeVersion":"24.12.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-748EcGqoMa6WfRM1OzHH5fJ0cxYJ5DnUY7zNKXZfZsvC7PK5hEXeu8cdc5AVNeCqM8s57O36qCNSPxQjLSvvMA==","shasum":"86cdd96f7b1c7b7304f91df3b58ae1de419a8539","tarball":"https://registry.npmjs.org/@dmytromykhailiuk/injectable/-/injectable-1.0.2.tgz","fileCount":9,"unpackedSize":106653,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDx9LHB9XE6yWT5/LzoWh0vxmr2Xb9pylppxX1Q7fR+9gIhAOrdTRVg8euapoXxIoXcuDiOdbWdYmAeLYcGIe+4C5cs"}]},"_npmUser":{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"},"directories":{},"maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/injectable_1.0.2_1786638520952_0.8525230820424421"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-08T12:11:35.477Z","modified":"2026-08-13T16:28:41.786Z","1.0.0":"2026-06-08T12:11:35.842Z","1.0.1":"2026-07-22T10:08:08.141Z","1.0.2":"2026-08-13T16:28:41.095Z"},"bugs":{"url":"https://github.com/dmytromykhailiuk/injectable/issues"},"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","homepage":"https://dmytromykhailiuk.github.io/injectable/","keywords":["dependency-injection","di","ioc","container","injectable","inject","provider","nested-container","hierarchical","multi-provider","scope","singleton","factory","service-locator","typescript","ts","zero-dependencies","no-decorators","esm","tree-shakeable","nestjs","angular"],"repository":{"type":"git","url":"git+https://github.com/dmytromykhailiuk/injectable.git"},"description":"Dependency injection with nested containers, full TypeScript typings, and an ergonomic, decorator-free API. Angular / NestJS providers without the framework.","maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"readme":"# @dmytromykhailiuk/injectable\n\n> **Full documentation:** open [Docs](https://dmytromykhailiuk.github.io/injectable/)\n\n**Dependency injection, without the framework.**\n\nIf you have used providers in Angular or NestJS and wished for the same mental model without the runtime around it, this is that model on its own: hierarchical containers, class / value / factory / alias providers, multi-providers, and typed injection tokens. No decorators, no `reflect-metadata`, no compiler flags. Zero dependencies.\n\n```ts\nimport {\n  createContainer,\n  inject,\n  createInjectionToken,\n} from \"@dmytromykhailiuk/injectable\";\n\nclass Logger {\n  log(message: string) {\n    console.log(`[log] ${message}`);\n  }\n}\n\nclass UserService {\n  // A dependency, declared where it is used — no constructor boilerplate.\n  private logger = inject(Logger);\n\n  greet(name: string) {\n    this.logger.log(`hello, ${name}`);\n  }\n}\n\nconst container = createContainer();\ncontainer.register(UserService, Logger); // order does not matter\n\ncontainer.get(UserService).greet(\"world\"); // [log] hello, world\n```\n\nThe whole surface is three functions — `createContainer`, `inject`, `createInjectionToken` — plus a handful of provider shapes and error classes. It reads in one sitting.\n\n---\n\n## Contents\n\n- [Install](#install)\n- [Why](#why)\n- [Quick start](#quick-start)\n- [Providers](#providers)\n- [Injection tokens](#injection-tokens)\n- [`inject()` vs `container.get()`](#inject-vs-containerget)\n- [Injection options](#injection-options)\n- [Nested containers](#nested-containers)\n- [Deferred registration](#deferred-registration)\n- [Lifecycle](#lifecycle)\n- [Recipes](#recipes)\n- [Compared to Angular DI](#compared-to-angular-di)\n- [Compared to NestJS DI](#compared-to-nestjs-di)\n- [API reference](#api-reference)\n- [Limitations](#limitations)\n- [Development](#development)\n\n---\n\n## Install\n\n```sh\nnpm i @dmytromykhailiuk/injectable\n```\n\nRequires Node **18+** and TypeScript **5.0+**. No `reflect-metadata`, no `experimentalDecorators`, nothing to enable in the consuming project. ESM and CJS builds ship side by side with separate `.d.ts` / `.d.cts` declarations, and the package is side-effect-free and tree-shakeable.\n\n### Three functions, and that is all\n\n```ts\nimport {\n  createContainer, // makes a container (optionally nested under a parent)\n  inject, // pulls a dependency while a provider is being built\n  createInjectionToken, // a typed key for anything that is not a class\n} from \"@dmytromykhailiuk/injectable\";\n```\n\nEverything else is a provider object you pass to `register()`, an option you pass to `inject()` / `get()`, or an error class you can `catch`.\n\n---\n\n## Why\n\nWiring objects together by hand does not scale. You end up threading the same `Logger` through six constructors to reach the one class that needs it, and every new dependency edits every call site on the way down. Frameworks solve this with a container — but adopting Angular or NestJS to get one is a lot of framework for one idea.\n\nThis library is just the container. The idea is the same one those frameworks use:\n\n- **A provider is a recipe for a value**, registered under a key. The key is a class, a string, or a typed token.\n- **`inject()` asks the container for a key** while a provider is being constructed. It takes no container argument — it resolves against whichever container is doing the building right now, exactly like Angular's `inject()`.\n- **Containers nest.** A child resolves from itself first, then falls back to its parent. That single rule gives you request scopes, feature modules, and test overrides for free.\n\nWhat you give up by not using a framework is decorators and reflection-based auto-wiring. What you get back is a container with no magic: providers are plain objects, resolution is a `Map` lookup up a parent chain, and the whole thing has no dependencies and one job.\n\n### What it is not\n\nNo decorators, no `reflect-metadata`, no module system, no lifecycle hooks beyond `destroy`, no async providers, no proxies. If you want those, Angular and NestJS are excellent and this is not trying to replace them — this is the resolution model underneath, on its own.\n\n---\n\n## Quick start\n\n```ts\nimport { createContainer, inject } from \"@dmytromykhailiuk/injectable\";\n\nclass Logger {\n  log(message: string) {\n    console.log(`[log] ${message}`);\n  }\n}\n\nclass UserService {\n  private logger = inject(Logger);\n  greet(name: string) {\n    this.logger.log(`hello, ${name}`);\n  }\n}\n\nconst container = createContainer();\ncontainer.register(UserService, Logger);\n\ncontainer.get(UserService).greet(\"world\");\n```\n\n`inject()` is valid **only while a provider is being instantiated** — inside a class constructor (a field initializer counts) or a `useCreate` factory. To pull an instance out from application code that holds the container, use `container.get()`.\n\n---\n\n## Providers\n\nAnything you pass to `container.register()` is a provider. Every provider is instantiated **once**, on registration, and the instance is cached — providers are singletons within their container.\n\n### Class provider\n\nThe simplest form. Pass the class; the container `new`s it and caches the result.\n\n```ts\nclass Mailer {}\n\ncontainer.register(Mailer);\ncontainer.get(Mailer); // the same instance every time\n```\n\n### `useValue` — bind a ready-made value\n\n```ts\nconst API_URL = createInjectionToken<string>(\"API_URL\");\n\ncontainer.register({ provide: API_URL, useValue: \"https://api.example.com\" });\n\ncontainer.get(API_URL); // \"https://api.example.com\", typed as string\n```\n\n### `useCreate` — factory or class\n\n`useCreate` takes a zero-argument factory **or** a class. Call `inject()` inside it to pull in other providers — that is how you wire what would otherwise be constructor arguments.\n\n```ts\nconst LOGGER = createInjectionToken<Logger>(\"LOGGER\");\n\ncontainer.register({\n  provide: LOGGER,\n  useCreate: () => {\n    const platform = inject(PlatformFacade);\n    return platform.isServer ? inject(ServerLogger) : inject(BrowserLogger);\n  },\n});\n```\n\nInjecting through default parameters works too, which reads nicely for classes:\n\n```ts\nclass Group {\n  constructor(private logger = inject<Logger>(LOGGER)) {}\n}\n```\n\n### `useExisting` — alias\n\nResolve another token now and store the **same** instance under a new key.\n\n```ts\ncontainer.register(Logger);\ncontainer.register({ provide: \"AppLogger\", useExisting: Logger });\n\ncontainer.get(\"AppLogger\") === container.get(Logger); // true\n```\n\n### `multi: true` — collect into an array\n\nRegister the same token several times with `multi: true` and read the values back as an array.\n\n```ts\nconst HOOKS = createInjectionToken<string>(\"HOOKS\");\n\ncontainer.register(\n  { provide: HOOKS, useValue: \"before\", multi: true },\n  { provide: HOOKS, useValue: \"after\", multi: true },\n);\n\ncontainer.get(HOOKS, { multi: true }); // [\"before\", \"after\"]\n```\n\nFor class-based multi providers there is an array sugar — `[Class]` — that both `inject()` and `get()` accept:\n\n```ts\nclass Middleware {}\n\ncontainer.register({ provide: Middleware, useCreate: Cors, multi: true });\ncontainer.register({ provide: Middleware, useCreate: Auth, multi: true });\n\ncontainer.get([Middleware]); // Middleware[]\ninject([Middleware]); // same, inside a factory\n```\n\n---\n\n## Injection tokens\n\nFor anything that is not a class — primitives, interfaces, abstract contracts — create a token. `createInjectionToken<T>()` returns a real `symbol`, so it can never collide with a string used elsewhere, and it carries `T` at the type level, so resolution infers the value type with no second generic.\n\n```ts\nimport { createInjectionToken } from \"@dmytromykhailiuk/injectable\";\n\ninterface Config {\n  retries: number;\n}\nconst CONFIG = createInjectionToken<Config>(\"CONFIG\");\n\ncontainer.register({ provide: CONFIG, useValue: { retries: 3 } });\n\ncontainer.get(CONFIG).retries; // 3 — typed, no `get<Config>` needed\n```\n\nThe `id` is a label for debugging only. Two tokens made with the same `id` are still distinct — symbols are compared by identity. Plain strings work as keys too (`container.get(\"CONFIG\")`), but a token is safer for anything beyond a quick prototype.\n\n---\n\n## `inject()` vs `container.get()`\n\nBoth resolve providers; they differ in where they are used.\n\n|                       | `inject(token, opts?)`                                   | `container.get(token, opts?)`         |\n| --------------------- | -------------------------------------------------------- | ------------------------------------- |\n| Called from           | a constructor or `useCreate` factory, during registration | application code holding the container |\n| Which container       | the one currently building — resolved implicitly         | the one you call it on                |\n| Outside registration  | throws `InjectOutOfContextError`                          | always valid                           |\n| Missing & not optional | **throws** (parks the provider being built)              | returns `undefined`                    |\n\n```ts\nclass OrderService {\n  private mailer = inject(Mailer); // OK — inside a constructor\n}\n\ninject(Mailer); // throws — no active container\ncontainer.get(Mailer); // OK — undefined if not registered\n```\n\n---\n\n## Injection options\n\nBoth `inject()` and `container.get()` accept the same options:\n\n```ts\ninterface InjectOptions {\n  host?: boolean; // resolve only from this container, ignore parents\n  skipSelf?: boolean; // skip this container, resolve from the parent chain\n  multi?: boolean; // treat the result as an array\n  optional?: boolean; // return undefined instead of throwing when missing\n}\n```\n\n```ts\n// With { optional: true } the return type widens to include undefined:\nconst analytics = inject(Analytics, { optional: true }) ?? new NoopAnalytics();\n\ncontainer.get(Logger, { skipSelf: true }); // explicitly use the parent's Logger\ncontainer.get(Logger, { host: true }); // only this container's Logger\n```\n\n`host` and `skipSelf` are the same flags Angular exposes, with the same meaning — see [Compared to Angular DI](#compared-to-angular-di).\n\n---\n\n## Nested containers\n\nPass a parent to `createContainer` and resolution becomes hierarchical: a child checks itself first, then falls back to the parent chain.\n\n```ts\nconst root = createContainer();\nroot.register({ provide: \"API_URL\", useValue: \"https://prod.example.com\" });\nroot.register(Logger);\n\nconst scope = createContainer(root);\nscope.register({ provide: \"API_URL\", useValue: \"http://localhost:3000\" });\n\nscope.get(\"API_URL\"); // \"http://localhost:3000\" — child wins\nscope.get(Logger); // inherited from root\n```\n\nFor `multi` providers the arrays merge — the child's values come first, then the parent's:\n\n```ts\nconst root = createContainer();\nroot.register({ provide: HOOKS, useValue: \"root\", multi: true });\n\nconst child = createContainer(root);\nchild.register({ provide: HOOKS, useValue: \"child\", multi: true });\n\nchild.get(HOOKS, { multi: true }); // [\"child\", \"root\"]\n```\n\n---\n\n## Deferred registration\n\nRegistration order does not matter. If a provider being built calls `inject()` for a token that has not been registered yet, the container **parks** that registration and re-runs it automatically as soon as the missing token arrives.\n\n```ts\nclass Db {}\nclass UserService {\n  private db = inject(Db);\n}\n\nconst c = createContainer();\nc.register(UserService); // Db missing — parked, not thrown\nc.register(Db); // arrival of Db re-runs UserService\n\nc.get(UserService); // ready\n```\n\nThis also works across the parent/child boundary — a child waits for a token its parent will register later. It is why `register(UserService, Logger)` resolves even though the dependent is listed before its dependency.\n\n---\n\n## Lifecycle\n\n### `destroy()`\n\nClears every instance, drops subscribers and pending registrations, detaches from the parent, and emits a `container-destroyed` event. Use it for per-request child scopes and test teardown.\n\n### `subscribe()`\n\nNotifies you when a provider registers and when the container is destroyed. The returned function unsubscribes. Registrations that happen in a parent propagate to a child's subscribers.\n\n```ts\nconst container = createContainer();\n\nconst unsubscribe = container.subscribe((event) => {\n  if (event.type === \"provider-registered\") {\n    console.log(\"registered:\", event.token.toString());\n  } else {\n    console.log(\"container destroyed\");\n  }\n});\n\ncontainer.register(Logger);\nunsubscribe();\ncontainer.destroy();\n```\n\n### `has()`\n\nReturns whether a token resolves in this container or any ancestor.\n\n```ts\ncontainer.has(Logger); // boolean\n```\n\n---\n\n## Recipes\n\n### Per-request scope\n\n```ts\nfunction handleRequest(req: Request) {\n  const scope = createContainer(rootContainer);\n  scope.register({ provide: \"REQ\", useValue: req });\n  try {\n    return scope.get(RequestHandler).run();\n  } finally {\n    scope.destroy();\n  }\n}\n```\n\n### Swap a real service for a fake in tests\n\n```ts\nconst test = createContainer(appContainer);\ntest.register({ provide: Mailer, useValue: new FakeMailer() });\n\nexpect(test.get(OrderService).checkout()).toMatchSnapshot();\n```\n\n### Group registrations as a \"module\"\n\n```ts\nexport function registerAuthModule(c: Container) {\n  c.register(PasswordHasher, TokenIssuer, {\n    provide: AuthService,\n    useCreate: () => new AuthService(inject(TokenIssuer), inject(PasswordHasher)),\n  });\n}\n\nregisterAuthModule(container);\n```\n\n### Pick an implementation at build time\n\n```ts\nconst STORAGE = createInjectionToken<Storage>(\"STORAGE\");\n\ncontainer.register(MemoryStorage, RedisStorage, {\n  provide: STORAGE,\n  useCreate: () =>\n    inject(Config).env === \"test\" ? inject(MemoryStorage) : inject(RedisStorage),\n});\n```\n\n---\n\n## Compared to Angular DI\n\nThe resolution model is deliberately the same as Angular's — `inject()`, hierarchical injectors, multi-providers, and the `host` / `skipSelf` / `optional` flags all mean what they mean in Angular. The difference is that there is no framework, no `NgModule`, no decorators, and no compiler step.\n\n**Angular**\n\n```ts\nimport { Injectable, InjectionToken, Injector, inject } from \"@angular/core\";\n\nconst API_URL = new InjectionToken<string>(\"API_URL\");\n\n@Injectable()\nclass Logger {\n  log(msg: string) {}\n}\n\n@Injectable()\nclass UserService {\n  private logger = inject(Logger);\n  private apiUrl = inject(API_URL);\n}\n\nconst injector = Injector.create({\n  providers: [\n    Logger,\n    UserService,\n    { provide: API_URL, useValue: \"https://api.example.com\" },\n  ],\n});\n\ninjector.get(UserService);\n```\n\n**This library**\n\n```ts\nimport { createContainer, createInjectionToken, inject } from \"@dmytromykhailiuk/injectable\";\n\nconst API_URL = createInjectionToken<string>(\"API_URL\");\n\nclass Logger {\n  log(msg: string) {}\n}\n\nclass UserService {\n  private logger = inject(Logger);\n  private apiUrl = inject(API_URL);\n}\n\nconst container = createContainer();\ncontainer.register(Logger, UserService, {\n  provide: API_URL,\n  useValue: \"https://api.example.com\",\n});\n\ncontainer.get(UserService);\n```\n\n| Concept                       | Angular                                   | This library                                     |\n| ----------------------------- | ----------------------------------------- | ------------------------------------------------ |\n| Field injection               | `inject(Dep)`                             | `inject(Dep)` — identical                        |\n| Token                         | `new InjectionToken<T>(\"x\")`             | `createInjectionToken<T>(\"x\")`                  |\n| Value provider                | `{ provide, useValue }`                  | `{ provide, useValue }` — identical              |\n| Factory provider              | `{ provide, useFactory, deps: [...] }`   | `{ provide, useCreate }`, deps via `inject()`    |\n| Alias provider                | `{ provide, useExisting }`               | `{ provide, useExisting }` — identical           |\n| Class provider                | `{ provide, useClass }` or the class     | the class, or `{ provide, useCreate: Class }`    |\n| Multi                         | `{ provide, useValue, multi: true }`     | `{ provide, useValue, multi: true }` — identical |\n| Hierarchical injectors        | parent/child injectors                    | `createContainer(parent)`                        |\n| Resolution modifiers          | `@Host` / `@SkipSelf` / `@Optional`      | `{ host }` / `{ skipSelf }` / `{ optional }`     |\n| Decorators / `reflect-metadata` | required                                | **none**                                         |\n| Constructor parameter injection | `constructor(private x: Dep)` + metadata | `inject()` — no metadata, no `deps` array        |\n\nThe one thing Angular does that this cannot is inject through **constructor parameter types** (`constructor(private dep: Dep)`), because that relies on `emitDecoratorMetadata`. Here dependencies are named explicitly with `inject()` — a default parameter (`constructor(private dep = inject(Dep))`) is the closest equivalent, and it needs no build step.\n\n---\n\n## Compared to NestJS DI\n\nNestJS wires dependencies through constructor parameter types, `@Injectable()` decorators, `reflect-metadata`, and a module graph. This library keeps the same provider vocabulary — `useValue`, `useClass`/`useCreate`, `useFactory`/`useCreate`, `useExisting`, custom tokens, multi-providers — but replaces the module graph with plain container objects and the decorators with explicit `inject()`.\n\n**NestJS**\n\n```ts\nimport { Injectable, Inject, Module } from \"@nestjs/common\";\n\nconst API_URL = \"API_URL\";\n\n@Injectable()\nclass Logger {}\n\n@Injectable()\nclass UserService {\n  constructor(\n    private readonly logger: Logger,\n    @Inject(API_URL) private readonly apiUrl: string,\n  ) {}\n}\n\n@Module({\n  providers: [\n    Logger,\n    UserService,\n    { provide: API_URL, useValue: \"https://api.example.com\" },\n  ],\n})\nclass AppModule {}\n```\n\n**This library**\n\n```ts\nimport { createContainer, inject } from \"@dmytromykhailiuk/injectable\";\n\nconst API_URL = \"API_URL\";\n\nclass Logger {}\n\nclass UserService {\n  private logger = inject(Logger);\n  private apiUrl = inject<string>(API_URL);\n}\n\nconst container = createContainer();\ncontainer.register(Logger, UserService, {\n  provide: API_URL,\n  useValue: \"https://api.example.com\",\n});\n```\n\n| Concept              | NestJS                                     | This library                                    |\n| -------------------- | ------------------------------------------ | ----------------------------------------------- |\n| Marking a class      | `@Injectable()`                            | nothing — any class is a provider               |\n| Injecting a class    | constructor param + metadata               | `inject(Dep)`                                   |\n| Injecting a token    | `@Inject(TOKEN) x`                         | `inject(TOKEN)`                                 |\n| Value provider       | `{ provide, useValue }`                    | `{ provide, useValue }` — identical             |\n| Class provider       | `{ provide, useClass }`                    | `{ provide, useCreate: Class }`                 |\n| Factory provider     | `{ provide, useFactory, inject: [...] }`   | `{ provide, useCreate }`, deps via `inject()`   |\n| Alias provider       | `{ provide, useExisting }`                 | `{ provide, useExisting }` — identical          |\n| Custom token         | a string or `Symbol`                       | `createInjectionToken()` (or a string / symbol) |\n| Module system        | `@Module({ providers, imports, exports })` | a plain function that calls `register()`        |\n| Request scope        | `Scope.REQUEST` + framework machinery      | `createContainer(parent)` per request           |\n| Runtime dependencies | `reflect-metadata`                         | **none**                                        |\n\nNestJS resolves the entire module graph at bootstrap and throws if anything is unresolvable. This library resolves lazily and **parks** an unmet dependency until it is registered (see [Deferred registration](#deferred-registration)) — which is more forgiving, but means a genuinely missing provider surfaces as `undefined` from `get()` rather than as a startup error.\n\n---\n\n## API reference\n\n```ts\n// Tokens & containers\ncreateInjectionToken<T = unknown>(id: string): InjectionToken<T>;\ncreateContainer(parent?: Container): Container;\n\ninterface Container {\n  register(...providers: ProviderOption[]): void;\n  get<T>(token: InjectionToken<T> | (new () => T), options?: InjectOptions): T;\n  get<T>(token: [new () => T], options?: InjectOptions): T[];\n  get<T = unknown>(token: string, options?: InjectOptions): T;\n  has(token: Provider | [ProviderClass]): boolean;\n  destroy(): void;\n  subscribe(fn: (event: ContainerEvent) => void): () => void;\n}\n\n// Injection — same call shape as get()\ninject<T>(token: InjectionToken<T> | (new () => T), options?: InjectOptions): T;\ninject<T>(token: [new () => T], options?: InjectOptions): T[];\ninject<T>(token: Token, options: { optional: true } & InjectOptions): T | undefined;\n\ninterface InjectOptions {\n  host?: boolean;\n  skipSelf?: boolean;\n  multi?: boolean;\n  optional?: boolean;\n}\n\n// Provider shapes\ntype ProviderOption<T = unknown> =\n  | (new () => T)\n  | { provide: Provider; useValue: T;                          multi?: boolean }\n  | { provide: Provider; useExisting: Provider<T>;             multi?: boolean }\n  | { provide: Provider; useCreate: (new () => T) | (() => T); multi?: boolean };\n\ntype Provider<T = unknown> = InjectionToken<T> | string | (new () => T);\n\n// Events\ntype ContainerEvent =\n  | { type: \"provider-registered\"; token: string | symbol }\n  | { type: \"container-destroyed\" };\n```\n\n### Errors\n\nAll are exported and can be caught with `instanceof`.\n\n| Class                            | Thrown when                                                                 |\n| -------------------------------- | --------------------------------------------------------------------------- |\n| `InjectOutOfContextError`        | `inject()` is called outside a `register()` factory or constructor.         |\n| `ProviderAlreadyRegisteredError` | the same non-multi token is registered twice in one container.              |\n| `MultiProviderConflictError`     | a token is registered — or read — both as multi and non-multi.              |\n| `EmptyTokenError`                | a provider is registered under an empty string token.                       |\n| `MissingProviderError`           | a required `inject()` cannot resolve (parks the provider being built).      |\n\n---\n\n## Limitations\n\n- **Synchronous only.** `useCreate` cannot return a `Promise`. Resolve async work upfront and register the result, or expose it behind a lazy method.\n- **No decorators.** `@Injectable` / `@Inject` are not part of this package by design. Dependencies are named explicitly with `inject()`.\n- **No constructor-type injection.** There is no `reflect-metadata`, so you cannot inject from a parameter's declared type — use `inject()` (a default parameter is the closest form).\n- **No module system.** Group providers with a plain function (see [Recipes](#recipes)).\n- **Circular dependencies do not resolve.** If two providers each `inject()` the other, both park forever and `get()` returns `undefined` for both — there is no cycle-detection error. Break the cycle with a factory that resolves one side lazily.\n- **A genuinely missing dependency is silent.** Because resolution is deferred, an unmet dependency reads as `undefined` from `get()` rather than throwing at startup. Use `has()` if you need to assert presence.\n\n---\n\n## Development\n\n```sh\nnpm run playground        # a runnable tour of every feature\nnpm run playground:watch  # the same, re-running on save\nnpm test                  # the full runtime test suite\nnpm run test:coverage     # tests with a coverage report\nnpm run typecheck         # tsc --noEmit\nnpm run lint              # biome\nnpm run verify            # lint + typecheck + test + build\n```\n\n## License\n\nMIT © Dmytro Mykhailiuk\n","readmeFilename":"README.md"}