katagami 3.0.0 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -584
- package/dist/chunk-J2NYR3SH.js +6 -0
- package/dist/container/index.d.cts +108 -0
- package/dist/container/index.d.ts +2 -2
- package/dist/disposable/index.cjs +16 -25
- package/dist/disposable/index.d.cts +69 -0
- package/dist/disposable/index.d.ts +13 -5
- package/dist/disposable/index.js +1 -1
- package/dist/error/index.d.cts +11 -0
- package/dist/index.cjs +91 -55
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +76 -31
- package/dist/internal.d.cts +29 -0
- package/dist/internal.d.ts +3 -1
- package/dist/lazy/index.cjs +16 -25
- package/dist/lazy/index.d.cts +33 -0
- package/dist/lazy/index.d.ts +3 -3
- package/dist/lazy/index.js +1 -1
- package/dist/resolver/index.d.cts +93 -0
- package/dist/scope/index.d.cts +120 -0
- package/dist/scope/index.d.ts +4 -4
- package/docs/README.de.md +84 -0
- package/docs/README.es.md +84 -0
- package/docs/README.fr.md +84 -0
- package/docs/README.ja.md +105 -0
- package/docs/README.ko.md +84 -0
- package/docs/README.zh-CN.md +84 -0
- package/docs/README.zh-TW.md +84 -0
- package/docs/ai-coding-agents.md +78 -0
- package/docs/articles/ai-coding-agents.ja.md +83 -0
- package/docs/articles/ai-coding-agents.md +70 -0
- package/docs/articles/request-scope.md +48 -0
- package/docs/articles/without-decorators.md +54 -0
- package/docs/choosing-di.md +143 -0
- package/docs/growth/baseline-2026-09-11.json +68 -0
- package/docs/growth/github-metadata.json +13 -0
- package/docs/growth/rollout.md +77 -0
- package/docs/guide.md +186 -0
- package/docs/type-safety.md +126 -0
- package/examples/request-scope/README.md +37 -0
- package/examples/request-scope/app.ts +31 -0
- package/examples/request-scope/demo.ts +10 -0
- package/examples/request-scope/tsconfig.json +11 -0
- package/llms.txt +16 -0
- package/package.json +56 -23
- package/dist/index-jx8b52m0.js +0 -4
package/README.md
CHANGED
|
@@ -2,618 +2,174 @@
|
|
|
2
2
|
|
|
3
3
|
# Katagami
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**Type-safe dependency injection for TypeScript.**
|
|
6
|
+
|
|
7
|
+
Make dependency wiring explicit and type-checked—even when AI coding agents write the code.
|
|
8
|
+
Katagami accumulates types as you register dependencies, checks which tokens a factory can access,
|
|
9
|
+
and tracks asynchronous results. No decorators, no reflect-metadata, no runtime dependencies.
|
|
6
10
|
|
|
7
11
|
[](https://www.npmjs.com/package/katagami)
|
|
12
|
+
[](https://github.com/hiroiku/katagami/actions/workflows/ci.yml)
|
|
8
13
|
[](https://github.com/hiroiku/katagami/blob/master/LICENSE)
|
|
9
|
-
[](https://bundlephobia.com/package/katagami)
|
|
10
|
-
|
|
11
|
-
> The name comes from 型紙 _(katagami)_ — precision stencil paper used in traditional Japanese dyeing to transfer exact patterns onto fabric. Multiple stencils are layered to compose intricate designs, just as types accumulate through each method-chain call. Each stencil is a self-contained piece — chosen only for the current work, the rest left behind — just as subpath exports ensure only the code you use enters your bundle. The cut pattern determines exactly where dye passes and where it is blocked, much like Katagami's type system catches misuse at compile time, not at runtime. And a stencil needs only paper and a brush, no elaborate machinery — likewise, Katagami requires no decorators or metadata mechanisms and works with any build tool out of the box.
|
|
12
|
-
|
|
13
|
-
## Features
|
|
14
|
-
|
|
15
|
-
| Feature | Description |
|
|
16
|
-
| ----------------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
17
|
-
| Zero dependencies | No decorators, no reflect-metadata, no polyfills — works with any bundler out of the box |
|
|
18
|
-
| Full type inference | Types accumulate through method chaining; unregistered tokens are compile-time errors |
|
|
19
|
-
| Tree-shakeable | Subpath exports (`katagami/disposable`, `katagami/lazy`) and `sideEffects: false` for minimal bundle size |
|
|
20
|
-
| Captive dependency prevention | Singleton/Transient factories cannot access scoped tokens; caught at compile time and runtime in scopes |
|
|
21
|
-
| Hybrid token strategy | Class tokens for strict type safety, PropertyKey tokens for flexibility |
|
|
22
|
-
| Interface type map | Pass an interface to `createContainer<T>()` for order-independent registration |
|
|
23
|
-
| Three lifetimes | Singleton, Transient, and Scoped with child containers |
|
|
24
|
-
| Disposable support | TC39 Explicit Resource Management (`Symbol.dispose` / `Symbol.asyncDispose` / `await using`) |
|
|
25
|
-
| Module composition | Containers can be composed via `use()` to group and reuse registrations |
|
|
26
|
-
| Async factories | Promise-returning factories are automatically tracked by the type system |
|
|
27
|
-
| Circular dependency detection | Clear error messages with the full cycle path |
|
|
28
|
-
| Optional resolution | `tryResolve` returns `undefined` for unregistered tokens instead of throwing |
|
|
29
|
-
| Lazy resolution | Proxy-based deferred instantiation via `lazy()` from `katagami/lazy`; instance created on first access |
|
|
30
|
-
|
|
31
|
-
## Install
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
npm install katagami
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## Quick Start
|
|
38
|
-
|
|
39
|
-
```ts
|
|
40
|
-
import { createContainer, createScope } from 'katagami';
|
|
41
|
-
|
|
42
|
-
class Logger {
|
|
43
|
-
log(msg: string) {
|
|
44
|
-
console.log(msg);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
class UserService {
|
|
49
|
-
constructor(private logger: Logger) {}
|
|
50
|
-
greet(name: string) {
|
|
51
|
-
this.logger.log(`Hello, ${name}`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const container = createContainer()
|
|
56
|
-
.registerSingleton(Logger, () => new Logger())
|
|
57
|
-
.registerSingleton(UserService, r => new UserService(r.resolve(Logger)));
|
|
58
|
-
|
|
59
|
-
const userService = createScope(container).resolve(UserService);
|
|
60
|
-
// ^? UserService (fully inferred)
|
|
61
|
-
userService.greet('world');
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
## Why Katagami
|
|
65
|
-
|
|
66
|
-
Most TypeScript DI containers rely on decorators, reflect-metadata, or string-based tokens — each bringing trade-offs in tooling compatibility, type safety, or bundle size. Katagami takes a different approach.
|
|
67
|
-
|
|
68
|
-
> **Note:** This comparison was researched on 2026-02-09. Features may have changed since then.
|
|
69
|
-
|
|
70
|
-
| Aspect | Katagami | InversifyJS | tsyringe | TypeDI | Awilix | NestJS | Effect | typed-inject |
|
|
71
|
-
| --------------------------------- | ---------------------------------------------------------------- | -------------------------------- | ----------------------------------------------- | --------------------------------------- | ----------------------------------- | ------------------------------------- | --------------------------------- | ------------------------------ |
|
|
72
|
-
| **Runtime requirements** | ✅ None | ❌ reflect-metadata, decorators | ❌ reflect-metadata, decorators | ❌ reflect-metadata, decorators | ✅ None | ❌ reflect-metadata, decorators | ✅ None | ✅ None |
|
|
73
|
-
| **Lifetimes** | ✅ Singleton, Transient, Scoped | ✅ Singleton, Transient, Request | ✅ Singleton, Transient, Resolution / Container | Singleton, Transient (named containers) | ✅ Singleton, Transient, Scoped | ✅ Singleton, Transient, Request | Shared (memoized), Scoped | ❌ Singleton, Transient |
|
|
74
|
-
| **Injection style** | Constructor (explicit factory) | Constructor, Property | Constructor | Constructor, Property | Constructor (proxy / classic) | Constructor | Functional (Tag + Layer) | Constructor (static inject) |
|
|
75
|
-
| **Token types** | Class, PropertyKey, Interface map | Class, String, Symbol | Class, String, Symbol | Class, String, Token\<T\> | String | Class, String, Symbol, InjectionToken | Context.Tag | String literal |
|
|
76
|
-
| **Type safety** | ✅ Compile-time; full inference, captive-dep guard (+ runtime) | ❌ Generic binding types | ❌ Generic types | ❌ Generic types, Token\<T\> | ❌ Cradle interface typing | ❌ Generic types | ✅ Compile-time; R type parameter | ✅ Compile-time; static inject |
|
|
77
|
-
| **Resource cleanup** | ✅ TC39 Symbol.dispose / asyncDispose; await using | Deactivation handlers | container.dispose() | Container.reset() | Disposer functions | Lifecycle hooks (onModuleDestroy) | Scope finalizers; acquireRelease | injector.dispose() |
|
|
78
|
-
| **Tree-shaking** | ✅ Subpath exports; sideEffects: false | ❌ | ❌ | ❌ | ✅ No decorator / metadata overhead | ❌ | ✅ Subpath exports; ESM | ✅ Zero deps; small bundle |
|
|
79
|
-
| **Async factories** | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ |
|
|
80
|
-
| **Optional resolution** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
|
|
81
|
-
| **Multi-binding** | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
82
|
-
| **Lazy resolution** | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ LazyModuleLoader | ✅ Lazy by design | ❌ |
|
|
83
|
-
| **Conditional bindings** | ✅ Token separation + factory logic + scopes | ✅ Named, tagged, contextual | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
|
84
|
-
| **Auto-loading** | ✅ use() module composition (explicit, decorator-free by design) | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
|
85
|
-
| **Child containers** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
86
|
-
| **Module system** | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ |
|
|
87
|
-
| **Circular dependency detection** | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ |
|
|
88
|
-
| **Middleware / Interceptors** | ✅ Higher-order factory wrappers | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
|
89
|
-
| **Snapshot / Restore** | ✅ Immutable containers; use() for test isolation | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
|
90
|
-
|
|
91
|
-
### No decorators, no reflect-metadata
|
|
92
|
-
|
|
93
|
-
Decorator-based DI requires `experimentalDecorators` and `emitDecoratorMetadata` compiler options. Modern build tools such as esbuild and Vite (default configuration) do not support `emitDecoratorMetadata`, and the TC39 standard decorators proposal does not include an equivalent for automatic type metadata emission. Katagami depends on none of these — it works with any build tool out of the box.
|
|
94
|
-
|
|
95
|
-
### Tree-shakeable
|
|
96
14
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
```ts
|
|
100
|
-
// Core only — disposable and lazy are not included in the bundle
|
|
101
|
-
import { createContainer, createScope } from 'katagami';
|
|
102
|
-
|
|
103
|
-
// Import only what you need
|
|
104
|
-
import { disposable } from 'katagami/disposable';
|
|
105
|
-
import { lazy } from 'katagami/lazy';
|
|
15
|
+
```sh
|
|
16
|
+
npm install katagami
|
|
106
17
|
```
|
|
107
18
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
String-token DI forces you to maintain manual token-to-type mappings. Parameter-name matching breaks under minification. Katagami uses classes directly as tokens, so `resolve` automatically infers the correct return type — synchronous or `Promise` — with no extra annotations.
|
|
111
|
-
|
|
112
|
-
### Method-chain type accumulation
|
|
113
|
-
|
|
114
|
-
Types accumulate with each `register` call. Inside a factory, the resolver only accepts tokens that have already been registered at that point in the chain. Resolving an unregistered token is a compile-time error, not a runtime surprise.
|
|
115
|
-
|
|
116
|
-
### Hybrid token strategy
|
|
117
|
-
|
|
118
|
-
Class tokens give you strict, order-dependent type safety through method chaining. But sometimes you want to define a set of services upfront and register them in any order. Pass an interface to `createContainer<T>()` and use PropertyKey tokens — the type map is fixed at creation time, so registration order does not matter.
|
|
119
|
-
|
|
120
|
-
### Zero dependencies
|
|
121
|
-
|
|
122
|
-
No runtime dependencies, no polyfills. No need to add reflect-metadata (~50 KB unminified) to your bundle.
|
|
123
|
-
|
|
124
|
-
## Guide
|
|
125
|
-
|
|
126
|
-
### Singleton & Transient
|
|
127
|
-
|
|
128
|
-
Singleton creates the instance on the first `resolve` and caches it. Transient creates a new instance every time.
|
|
19
|
+
## Quick start
|
|
129
20
|
|
|
130
21
|
```ts
|
|
131
22
|
import { createContainer, createScope } from 'katagami';
|
|
132
23
|
|
|
133
|
-
class Database {
|
|
134
|
-
constructor(public id = Math.random()) {}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
class RequestHandler {
|
|
138
|
-
constructor(public id = Math.random()) {}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
24
|
const container = createContainer()
|
|
142
|
-
|
|
143
|
-
|
|
25
|
+
.registerSingleton('logger', () => ({ log: (message: string) => console.log(message) }))
|
|
26
|
+
.registerScoped('greeting', r => {
|
|
27
|
+
const logger = r.resolve('logger'); // Inferred from the previous registration
|
|
28
|
+
return (name: string) => logger.log(`Hello, ${name}!`);
|
|
29
|
+
});
|
|
144
30
|
|
|
145
31
|
const scope = createScope(container);
|
|
146
|
-
|
|
147
|
-
//
|
|
148
|
-
scope.resolve(Database) === scope.resolve(Database); // true
|
|
149
|
-
|
|
150
|
-
// Transient — new instance every time
|
|
151
|
-
scope.resolve(RequestHandler) === scope.resolve(RequestHandler); // false
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
### Scoped Lifetime & Child Containers
|
|
155
|
-
|
|
156
|
-
Scoped registrations behave like singletons within a scope but produce a fresh instance in each new scope. Use `createScope` to create a child container. Scoped tokens cannot be resolved from the root container.
|
|
157
|
-
|
|
158
|
-
```ts
|
|
159
|
-
import { createContainer, createScope } from 'katagami';
|
|
160
|
-
|
|
161
|
-
class DbPool {
|
|
162
|
-
constructor(public name = 'main') {}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
class RequestContext {
|
|
166
|
-
constructor(public id = Math.random()) {}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const root = createContainer()
|
|
170
|
-
.registerSingleton(DbPool, () => new DbPool())
|
|
171
|
-
.registerScoped(RequestContext, () => new RequestContext());
|
|
172
|
-
|
|
173
|
-
// Create a scope for each request
|
|
174
|
-
const scope1 = createScope(root);
|
|
175
|
-
const scope2 = createScope(root);
|
|
176
|
-
|
|
177
|
-
// Scoped — same within a scope, different across scopes
|
|
178
|
-
scope1.resolve(RequestContext) === scope1.resolve(RequestContext); // true
|
|
179
|
-
scope1.resolve(RequestContext) === scope2.resolve(RequestContext); // false
|
|
180
|
-
|
|
181
|
-
// Singleton — shared across all scopes
|
|
182
|
-
scope1.resolve(DbPool) === scope2.resolve(DbPool); // true
|
|
32
|
+
scope.resolve('greeting')('world');
|
|
33
|
+
// scope.resolve('missing'); // Type error: this token has not been registered
|
|
183
34
|
```
|
|
184
35
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const parentScope = createScope(root);
|
|
189
|
-
const childScope = createScope(parentScope);
|
|
190
|
-
|
|
191
|
-
// Each nested scope gets its own scoped instances
|
|
192
|
-
parentScope.resolve(RequestContext) === childScope.resolve(RequestContext); // false
|
|
193
|
-
|
|
194
|
-
// Singletons are still shared
|
|
195
|
-
parentScope.resolve(DbPool) === childScope.resolve(DbPool); // true
|
|
196
|
-
```
|
|
36
|
+
No service interface or explicit generic argument is needed here. Literal string keys,
|
|
37
|
+
unique symbols and class tokens can all be used. See [type guarantees](./docs/type-safety.md)
|
|
38
|
+
for the difference between accumulated registrations and a predeclared type map.
|
|
197
39
|
|
|
198
|
-
|
|
40
|
+
## Why Katagami
|
|
199
41
|
|
|
200
|
-
|
|
42
|
+
Katagami combines **registration-derived types, compile-time scope restrictions and zero runtime
|
|
43
|
+
dependencies** in an ordinary TypeScript factory API.
|
|
44
|
+
|
|
45
|
+
- **Types grow with your registrations.** Literal keys and unique symbols carry their inferred
|
|
46
|
+
service types into subsequent factories; required tokens outside that set are compile-time errors.
|
|
47
|
+
- **Request state stays explicit.** Singleton and transient factories cannot resolve scoped tokens
|
|
48
|
+
through their supplied typed resolver. You can find this mistake before starting the application.
|
|
49
|
+
- **No decorator setup.** No `experimentalDecorators`, `emitDecoratorMetadata` or Reflect polyfill
|
|
50
|
+
is needed for DI. Constructors and factories stay ordinary TypeScript.
|
|
51
|
+
- **Import the capabilities you use.** Core DI, `katagami/disposable` and `katagami/lazy` are separate
|
|
52
|
+
entry points. ESM exports and `sideEffects: false` support tree shaking; optional cleanup integrates
|
|
53
|
+
with `await using` and the host's disposal symbols.
|
|
54
|
+
|
|
55
|
+
These checks assume narrow tokens and preserved registration types; see the
|
|
56
|
+
[class-token, mutation and predeclared-map boundaries](./docs/type-safety.md).
|
|
57
|
+
|
|
58
|
+
### Library comparison
|
|
59
|
+
|
|
60
|
+
Reviewed **2026-09-11**, against the npm `latest` versions below and official documentation.
|
|
61
|
+
The tables describe the built-in APIs; add-ons and application-specific wrappers can change the
|
|
62
|
+
trade-offs. [Version sources and detailed comparison notes](./docs/choosing-di.md#comparison-sources)
|
|
63
|
+
explain the distinctions.
|
|
64
|
+
|
|
65
|
+
| Library / reviewed version | Dependency typing and missing registrations | Scoped-dependency handling | DI setup |
|
|
66
|
+
| --- | --- | --- | --- |
|
|
67
|
+
| **Katagami 3.0.2** | **Accumulated literal/unique-symbol tokens; missing required tokens rejected** | **Scoped tokens excluded from singleton/transient factory resolvers** | **No decorators or metadata; zero runtime dependencies** |
|
|
68
|
+
| [InversifyJS 8.2.3](https://inversify.io/docs/fundamentals/binding/) | Typed identifiers and bindings; binding existence checked at runtime | Declared binding scopes; not a scope-filtered resolver type | Metadata-based class injection; explicit value/factory bindings also available |
|
|
69
|
+
| [tsyringe 4.10.0](https://github.com/microsoft/tsyringe#readme) | Class/generic resolution types; registrations checked at runtime | Runtime lifetime settings; factory receives the container | Decorators and a Reflect metadata polyfill for class injection |
|
|
70
|
+
| [TypeDI 0.10.0](https://github.com/typestack/typedi/tree/v0.10.0) | Class and `Token<T>` types; registrations checked at runtime | Shared/transient services and named containers | Decorators and `reflect-metadata` in the TypeScript setup |
|
|
71
|
+
| [Awilix 13.0.5](https://github.com/jeffijoe/awilix#readme) | Registration-derived cradle types; broad `resolve` overload still accepts unknown names | `strict: true` checks lifetime leaks at runtime | No decorators or metadata |
|
|
72
|
+
| [NestJS 12.0.1](https://docs.nestjs.com/fundamentals/custom-providers) | Typed providers; module/provider graph resolved at runtime | Request scope propagates to dependent providers | Framework modules and metadata-based class injection |
|
|
73
|
+
| [Effect 3.22.2](https://effect.website/docs/v3/requirements-management/layers) | Service requirements tracked in `Effect` / `Layer` types | Typed `Scope` requirements and resource finalizers; a different lifetime model | No decorators or metadata; Effect's service/layer model |
|
|
74
|
+
| [typed-inject 5.0.0](https://github.com/nicojs/typed-inject#readme) | Accumulated string tokens and checked `inject` tuples | Singleton/transient providers and child injectors; no separate scoped lifetime | No decorators or metadata; zero runtime dependencies |
|
|
75
|
+
|
|
76
|
+
Awilix's inferred `cradle` and typed-inject's registration types are real compile-time features.
|
|
77
|
+
Katagami's distinction is the combination of accumulated registration checks with **scope-filtered
|
|
78
|
+
factory resolvers**, using direct `r.resolve(token)` calls. Effect also checks unsatisfied service
|
|
79
|
+
requirements, within its broader effect and resource model.
|
|
80
|
+
|
|
81
|
+
| Library | Lifetimes / scope model | Asynchronous services | Resource cleanup |
|
|
82
|
+
| --- | --- | --- | --- |
|
|
83
|
+
| **Katagami** | **Singleton, transient, scoped; nested scopes** | **Inferred `Promise<T>`; explicitly await dependencies** | **`Symbol.dispose` / `Symbol.asyncDispose` via `disposable()`; `await using`** |
|
|
84
|
+
| InversifyJS | Singleton, transient, request (one resolution graph); container hierarchy | Async bindings via `getAsync` / `getAllAsync`; awaits dependencies | Singleton deactivation handlers |
|
|
85
|
+
| tsyringe | Singleton, transient, resolution-scoped, container-scoped | Factories can return Promise-valued services; consumer handles the Promise | `container.dispose()` for constructed disposable instances |
|
|
86
|
+
| TypeDI | Shared or transient services; named containers | Promise-valued services; consumer handles the Promise | `reset()` / removal can call `destroy()`; returned Promise is not awaited |
|
|
87
|
+
| Awilix | Singleton, transient, scoped | Promise-valued factories; consumer handles the Promise | Registered disposers for cached singleton/scoped values |
|
|
88
|
+
| NestJS | Singleton, transient, HTTP request; scope propagation | Async providers are awaited before dependent construction | Application lifecycle hooks; not called for request-scoped classes |
|
|
89
|
+
| Effect | Memoized layers and explicit resource scopes | Effectful acquisition, including async effects | Scope finalizers and `acquireRelease` |
|
|
90
|
+
| typed-inject | Singleton, transient; disposable child injectors | Promise-valued factories with inferred return types | `injector.dispose()` awaits owned instances' `dispose()` |
|
|
91
|
+
|
|
92
|
+
“Request” is not identical across libraries: InversifyJS uses a resolution graph, while NestJS can
|
|
93
|
+
use an HTTP request. Katagami and Awilix let you create an explicit scope per request.
|
|
94
|
+
Returning a Promise and automatically awaiting dependencies are also different capabilities.
|
|
95
|
+
See [composition, optional/multiple resolution and tooling](./docs/choosing-di.md#composition-and-tooling)
|
|
96
|
+
for the rest of the feature comparison.
|
|
97
|
+
|
|
98
|
+
## Why Katagami for AI-assisted development?
|
|
99
|
+
|
|
100
|
+
Give coding agents a concrete feedback loop: edit dependency wiring, run the TypeScript checker,
|
|
101
|
+
and use the diagnostics to fix invalid dependencies. Explicit factories keep dependency edges
|
|
102
|
+
in ordinary TypeScript code that both people and agents can read.
|
|
103
|
+
|
|
104
|
+
For example, a singleton must not capture state that belongs to one request:
|
|
201
105
|
|
|
202
106
|
```ts
|
|
203
107
|
import { createContainer } from 'katagami';
|
|
204
108
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
class TokenService {
|
|
212
|
-
issue() {
|
|
213
|
-
return 'token';
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
class UserService {
|
|
218
|
-
constructor(private auth: AuthService, private tokens: TokenService) {}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Define a reusable module
|
|
222
|
-
const authModule = createContainer()
|
|
223
|
-
.registerSingleton(AuthService, () => new AuthService())
|
|
224
|
-
.registerSingleton(TokenService, () => new TokenService());
|
|
225
|
-
|
|
226
|
-
// Compose modules
|
|
227
|
-
const container = createContainer()
|
|
228
|
-
.use(authModule)
|
|
229
|
-
.registerSingleton(UserService, r => new UserService(r.resolve(AuthService), r.resolve(TokenService)));
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
Modules can also compose other modules:
|
|
233
|
-
|
|
234
|
-
```ts
|
|
235
|
-
const infraModule = createContainer().registerSingleton(AuthService, () => new AuthService());
|
|
236
|
-
|
|
237
|
-
const appModule = createContainer()
|
|
238
|
-
.use(infraModule)
|
|
239
|
-
.registerSingleton(UserService, r => new UserService(r.resolve(AuthService), r.resolve(TokenService)));
|
|
240
|
-
|
|
241
|
-
// appModule includes both AuthService and UserService
|
|
242
|
-
const container = createContainer().use(appModule);
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
### Async Factories
|
|
246
|
-
|
|
247
|
-
Factories that return a `Promise` are automatically tracked by the type system. When you `resolve` an async token, the return type is `Promise<V>` instead of `V`:
|
|
248
|
-
|
|
249
|
-
```ts
|
|
250
|
-
import { createContainer, createScope } from 'katagami';
|
|
251
|
-
|
|
252
|
-
class Database {
|
|
253
|
-
constructor(public connected: boolean) {}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
class Logger {
|
|
257
|
-
log(msg: string) {
|
|
258
|
-
console.log(msg);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const container = createContainer()
|
|
263
|
-
.registerSingleton(Logger, () => new Logger())
|
|
264
|
-
.registerSingleton(Database, async () => {
|
|
265
|
-
await new Promise(r => setTimeout(r, 100)); // simulate async init
|
|
266
|
-
return new Database(true);
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
const scope = createScope(container);
|
|
270
|
-
|
|
271
|
-
const logger = scope.resolve(Logger);
|
|
272
|
-
// ^? Logger
|
|
273
|
-
|
|
274
|
-
const db = await scope.resolve(Database);
|
|
275
|
-
// ^? Promise<Database> (awaited → Database)
|
|
276
|
-
db.connected; // true
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
Async factories can depend on both sync and async registrations:
|
|
280
|
-
|
|
281
|
-
```ts
|
|
282
|
-
const container = createContainer()
|
|
283
|
-
.registerSingleton(Logger, () => new Logger())
|
|
284
|
-
.registerSingleton(Database, async r => {
|
|
285
|
-
const logger = r.resolve(Logger); // sync → Logger
|
|
286
|
-
logger.log('Connecting...');
|
|
287
|
-
return new Database(true);
|
|
288
|
-
});
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
### Circular Dependency Detection
|
|
292
|
-
|
|
293
|
-
Katagami tracks which tokens are currently being resolved. If a circular dependency is found, a `ContainerError` is thrown with a clear message showing the full cycle path:
|
|
294
|
-
|
|
295
|
-
```ts
|
|
296
|
-
import { createContainer, createScope } from 'katagami';
|
|
297
|
-
|
|
298
|
-
class ServiceA {
|
|
299
|
-
constructor(public b: ServiceB) {}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
class ServiceB {
|
|
303
|
-
constructor(public a: ServiceA) {}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const container = createContainer()
|
|
307
|
-
.registerSingleton(ServiceA, r => new ServiceA(r.resolve(ServiceB)))
|
|
308
|
-
.registerSingleton(ServiceB, r => new ServiceB(r.resolve(ServiceA)));
|
|
309
|
-
|
|
310
|
-
createScope(container).resolve(ServiceA);
|
|
311
|
-
// ContainerError: Circular dependency detected: ServiceA -> ServiceB -> ServiceA
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
Indirect cycles are also detected:
|
|
315
|
-
|
|
316
|
-
```
|
|
317
|
-
ContainerError: Circular dependency detected: ServiceX -> ServiceY -> ServiceZ -> ServiceX
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
### Disposable Support
|
|
321
|
-
|
|
322
|
-
Disposal is provided by the `disposable()` wrapper from `katagami/disposable`. Wrapping a container or scope attaches `[Symbol.asyncDispose]`, enabling `await using` syntax. When disposed, owned instances are iterated in reverse creation order (LIFO) and their `[Symbol.asyncDispose]()` or `[Symbol.dispose]()` methods are called automatically.
|
|
323
|
-
|
|
324
|
-
```ts
|
|
325
|
-
import { createContainer, createScope } from 'katagami';
|
|
326
|
-
import { disposable } from 'katagami/disposable';
|
|
327
|
-
|
|
328
|
-
class Connection {
|
|
329
|
-
async [Symbol.asyncDispose]() {
|
|
330
|
-
console.log('Connection closed');
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Manual disposal
|
|
335
|
-
const container = createContainer().registerSingleton(Connection, () => new Connection());
|
|
336
|
-
const dc = disposable(container);
|
|
337
|
-
|
|
338
|
-
createScope(container).resolve(Connection);
|
|
339
|
-
await dc[Symbol.asyncDispose]();
|
|
340
|
-
// => "Connection closed"
|
|
109
|
+
createContainer()
|
|
110
|
+
.registerScoped('request', () => ({ id: crypto.randomUUID() }))
|
|
111
|
+
// @ts-expect-error — a singleton factory cannot resolve this scoped token
|
|
112
|
+
.registerSingleton('handler', r => r.resolve('request'));
|
|
341
113
|
```
|
|
342
114
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
```ts
|
|
346
|
-
import { createContainer, createScope } from 'katagami';
|
|
347
|
-
import { disposable } from 'katagami/disposable';
|
|
348
|
-
|
|
349
|
-
const root = createContainer()
|
|
350
|
-
.registerSingleton(DbPool, () => new DbPool())
|
|
351
|
-
.registerScoped(Connection, () => new Connection());
|
|
352
|
-
|
|
353
|
-
{
|
|
354
|
-
await using scope = disposable(createScope(root));
|
|
355
|
-
const conn = scope.resolve(Connection);
|
|
356
|
-
// ... use conn ...
|
|
357
|
-
} // scope is disposed here — Connection is cleaned up, DbPool is not
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
Scope disposal only affects scoped instances. Singleton instances are owned by the root container and are disposed when the container itself is disposed.
|
|
361
|
-
|
|
362
|
-
The `disposable()` wrapper also narrows the returned type so that registration methods (`registerSingleton`, `registerTransient`, `registerScoped`, `use`) are removed at the type level. This prevents accidental registration on a potentially-disposed container:
|
|
363
|
-
|
|
364
|
-
```ts
|
|
365
|
-
const dc = disposable(createContainer().registerSingleton(Connection, () => new Connection()));
|
|
366
|
-
|
|
367
|
-
dc.registerSingleton(/* ... */); // Compile-time error — registration methods are hidden
|
|
368
|
-
```
|
|
369
|
-
|
|
370
|
-
### Lazy Resolution
|
|
371
|
-
|
|
372
|
-
The `lazy()` function from `katagami/lazy` creates a proxy that defers instance creation until the first property access. This is useful for optimizing startup time or breaking circular dependencies.
|
|
373
|
-
|
|
374
|
-
```ts
|
|
375
|
-
import { createContainer, createScope } from 'katagami';
|
|
376
|
-
import { lazy } from 'katagami/lazy';
|
|
377
|
-
|
|
378
|
-
class HeavyService {
|
|
379
|
-
constructor() {
|
|
380
|
-
// expensive initialization
|
|
381
|
-
}
|
|
382
|
-
process() {
|
|
383
|
-
return 'done';
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const container = createContainer().registerSingleton(HeavyService, () => new HeavyService());
|
|
388
|
-
const scope = createScope(container);
|
|
389
|
-
|
|
390
|
-
const service = lazy(scope, HeavyService);
|
|
391
|
-
// HeavyService is NOT instantiated yet
|
|
392
|
-
|
|
393
|
-
service.process(); // instance created here, then cached
|
|
394
|
-
service.process(); // uses the cached instance
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
The proxy transparently forwards all property access, method calls, `in` checks, and prototype lookups to the real instance. Methods are automatically bound to the real instance, so `this` works correctly even when destructured.
|
|
398
|
-
|
|
399
|
-
Only **sync class tokens** are supported. Async tokens and PropertyKey tokens are rejected at the type level because Proxy traps are synchronous.
|
|
400
|
-
|
|
401
|
-
`lazy()` works with Scope and DisposableScope:
|
|
402
|
-
|
|
403
|
-
```ts
|
|
404
|
-
const root = createContainer().registerScoped(RequestContext, () => new RequestContext());
|
|
405
|
-
const scope = createScope(root);
|
|
406
|
-
|
|
407
|
-
const ctx = lazy(scope, RequestContext); // deferred scoped resolution
|
|
408
|
-
```
|
|
409
|
-
|
|
410
|
-
### Tree Shaking
|
|
411
|
-
|
|
412
|
-
Katagami uses subpath exports to split functionality into independent entry points. If you only need the core container, `katagami/disposable` and `katagami/lazy` are completely excluded from the bundle. The package declares `sideEffects: false`, so bundlers can safely eliminate any unused code.
|
|
413
|
-
|
|
414
|
-
```ts
|
|
415
|
-
// Core only — disposable and lazy are not included in the bundle
|
|
416
|
-
import { createContainer, createScope } from 'katagami';
|
|
417
|
-
|
|
418
|
-
// Import only what you need
|
|
419
|
-
import { disposable } from 'katagami/disposable';
|
|
420
|
-
import { lazy } from 'katagami/lazy';
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
### Interface Type Map
|
|
424
|
-
|
|
425
|
-
When you pass an interface to `createContainer<T>()`, PropertyKey tokens are typed from the interface rather than accumulated through chaining. This means you can register and resolve tokens in any order:
|
|
426
|
-
|
|
427
|
-
```ts
|
|
428
|
-
import { createContainer, createScope } from 'katagami';
|
|
429
|
-
|
|
430
|
-
class Logger {
|
|
431
|
-
log(msg: string) {
|
|
432
|
-
console.log(msg);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
interface Services {
|
|
437
|
-
logger: Logger;
|
|
438
|
-
greeting: string;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
const container = createContainer<Services>()
|
|
442
|
-
// 'greeting' can reference 'logger' even though it is registered later
|
|
443
|
-
.registerSingleton('greeting', r => {
|
|
444
|
-
r.resolve('logger').log('Building greeting...');
|
|
445
|
-
return 'Hello!';
|
|
446
|
-
})
|
|
447
|
-
.registerSingleton('logger', () => new Logger());
|
|
448
|
-
|
|
449
|
-
const greeting = createScope(container).resolve('greeting');
|
|
450
|
-
// ^? string
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
### Hybrid Token Strategy
|
|
454
|
-
|
|
455
|
-
You can mix both approaches — use class tokens for order-dependent type safety and PropertyKey tokens for order-independent flexibility:
|
|
456
|
-
|
|
457
|
-
```ts
|
|
458
|
-
const container = createContainer<Services>()
|
|
459
|
-
.registerSingleton(Logger, () => new Logger())
|
|
460
|
-
.registerSingleton('logger', () => new Logger())
|
|
461
|
-
.registerSingleton('greeting', r => {
|
|
462
|
-
r.resolve(Logger).log('Building greeting...');
|
|
463
|
-
return 'Hello!';
|
|
464
|
-
});
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
### Captive Dependency Prevention
|
|
468
|
-
|
|
469
|
-
A "captive dependency" occurs when a long-lived service (singleton or transient) captures a short-lived service (scoped), keeping it alive beyond its intended scope. Katagami prevents this at compile time — singleton and transient factories only receive a resolver limited to non-scoped tokens:
|
|
115
|
+
The compiler reports `No overload matches this call` at `r.resolve('request')`.
|
|
116
|
+
The factory's resolver does not include scoped tokens. Give the handler the same lifetime as the request:
|
|
470
117
|
|
|
471
118
|
```ts
|
|
472
119
|
import { createContainer } from 'katagami';
|
|
473
120
|
|
|
474
|
-
class DbPool {}
|
|
475
|
-
class RequestContext {}
|
|
476
|
-
|
|
477
121
|
const container = createContainer()
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
const container = createContainer().registerSingleton(Logger, () => new Logger());
|
|
531
|
-
const scope = createScope(container);
|
|
532
|
-
|
|
533
|
-
// resolve throws for unregistered tokens
|
|
534
|
-
scope.resolve(Analytics); // ContainerError: Token "Analytics" is not registered.
|
|
535
|
-
|
|
536
|
-
// tryResolve returns undefined for unregistered tokens
|
|
537
|
-
const analytics = scope.tryResolve(Analytics);
|
|
538
|
-
// ^? Analytics | undefined
|
|
539
|
-
if (analytics) {
|
|
540
|
-
analytics.track('event');
|
|
541
|
-
}
|
|
542
|
-
```
|
|
543
|
-
|
|
544
|
-
`tryResolve` is especially useful for optional dependencies in factories. Unlike `resolve`, it accepts unregistered tokens without compile-time errors:
|
|
545
|
-
|
|
546
|
-
```ts
|
|
547
|
-
const container = createContainer()
|
|
548
|
-
.registerSingleton(Logger, () => new Logger())
|
|
549
|
-
.registerSingleton('UserService', r => {
|
|
550
|
-
const logger = r.tryResolve(Logger); // Optional dependency
|
|
551
|
-
const analytics = r.tryResolve(Analytics); // No compile error even though Analytics is not registered
|
|
552
|
-
|
|
553
|
-
return {
|
|
554
|
-
greet(name: string) {
|
|
555
|
-
logger?.log(`Hello, ${name}`);
|
|
556
|
-
analytics?.track('user_greeted');
|
|
557
|
-
},
|
|
558
|
-
};
|
|
559
|
-
});
|
|
560
|
-
```
|
|
561
|
-
|
|
562
|
-
`tryResolve` still throws `ContainerError` for circular dependencies and operations on disposed containers/scopes — only unregistered tokens return `undefined`.
|
|
563
|
-
|
|
564
|
-
## API
|
|
565
|
-
|
|
566
|
-
### `createContainer<T, ScopedT>()`
|
|
567
|
-
|
|
568
|
-
Creates a new DI container. Pass an interface as `T` to define the type map for PropertyKey tokens. Pass `ScopedT` to define a separate type map for scoped PropertyKey tokens (order-independent, just like `T`).
|
|
569
|
-
|
|
570
|
-
### `Container.prototype.registerSingleton(token, factory)`
|
|
571
|
-
|
|
572
|
-
Registers a factory as a singleton. The instance is created on the first `resolve` and cached thereafter. Returns the container for method chaining.
|
|
573
|
-
|
|
574
|
-
### `Container.prototype.registerTransient(token, factory)`
|
|
575
|
-
|
|
576
|
-
Registers a factory as transient. A new instance is created on every `resolve`. Returns the container for method chaining.
|
|
577
|
-
|
|
578
|
-
### `Container.prototype.registerScoped(token, factory)`
|
|
579
|
-
|
|
580
|
-
Registers a factory as scoped. Within a scope, the instance is created on the first `resolve` and cached for that scope. Each scope maintains its own cache. Scoped tokens cannot be resolved from the root container. Returns the container for method chaining.
|
|
581
|
-
|
|
582
|
-
### `Container.prototype.use(source)`
|
|
583
|
-
|
|
584
|
-
Copies all registrations from `source` (another `Container`) into this container. Only factory and lifetime entries are copied — singleton instance caches are not shared. Returns the container for method chaining.
|
|
585
|
-
|
|
586
|
-
### `createScope(source)`
|
|
587
|
-
|
|
588
|
-
Creates a new `Scope` (child container) from a `Container` or an existing `Scope`. The scope inherits all registrations from the source. Singleton instances are shared with the parent, while scoped instances are local to the scope.
|
|
589
|
-
|
|
590
|
-
### `class Scope`
|
|
591
|
-
|
|
592
|
-
A scoped child container created by `createScope()`.
|
|
593
|
-
|
|
594
|
-
### `Scope.prototype.resolve(token)`
|
|
595
|
-
|
|
596
|
-
Resolves and returns the instance for the given token. Throws `ContainerError` if the token is not registered or if a circular dependency is detected. Can resolve both non-scoped and scoped tokens.
|
|
597
|
-
|
|
598
|
-
### `Scope.prototype.tryResolve(token)`
|
|
599
|
-
|
|
600
|
-
Attempts to resolve the instance for the given token. Returns `undefined` if the token is not registered, instead of throwing. Still throws `ContainerError` for circular dependencies or operations on disposed scopes.
|
|
601
|
-
|
|
602
|
-
### `lazy(source, token)` — `katagami/lazy`
|
|
603
|
-
|
|
604
|
-
Creates a Proxy that defers `resolve()` until the first property access. The resolved instance is cached — subsequent accesses use the cache. Only sync class tokens are supported; async tokens and PropertyKey tokens are rejected at the type level. Works with `Scope` and `DisposableScope`.
|
|
605
|
-
|
|
606
|
-
### `disposable(container)` — `katagami/disposable`
|
|
607
|
-
|
|
608
|
-
Attaches `[Symbol.asyncDispose]` to a `Container` or `Scope`, enabling `await using` syntax. Disposes all owned instances in reverse creation order (LIFO). Calls `[Symbol.asyncDispose]()` or `[Symbol.dispose]()` on each instance that implements them. Idempotent — subsequent calls are no-ops. After disposal, `resolve()` and `createScope()` will throw `ContainerError`. The returned type is narrowed to `DisposableContainer` or `DisposableScope`. `DisposableContainer` exposes only disposal capability — registration and resolution methods are excluded. `DisposableScope` retains `resolve`, `tryResolve`, `resolveAll`, and `tryResolveAll` for resolution.
|
|
609
|
-
|
|
610
|
-
### `class ContainerError`
|
|
611
|
-
|
|
612
|
-
Error class thrown for container failures such as resolving an unregistered token, circular dependencies, or operations on a disposed container/scope.
|
|
613
|
-
|
|
614
|
-
### `type Resolver`
|
|
615
|
-
|
|
616
|
-
Type export representing the resolver passed to factory callbacks. Useful when you need to type a function that accepts a resolver parameter.
|
|
122
|
+
.registerScoped('request', () => ({ id: crypto.randomUUID() }))
|
|
123
|
+
.registerScoped('handler', r => r.resolve('request'));
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The `@ts-expect-error` above makes the deliberately invalid example a checked regression test.
|
|
127
|
+
In application code, fix the lifetime and run `npx tsc --noEmit`; do not add a suppression.
|
|
128
|
+
These examples prove specific compiler checks. Improvements in agent success rate or token usage
|
|
129
|
+
have not been measured; the [evaluation protocol](https://github.com/hiroiku/katagami/tree/master/benchmarks/agent-wiring)
|
|
130
|
+
describes how to test them.
|
|
131
|
+
|
|
132
|
+
**Start here:** [Guide for AI coding agents](./docs/ai-coding-agents.md) ·
|
|
133
|
+
[Runnable request-scope starter](./examples/request-scope/README.md) ·
|
|
134
|
+
[Type safety and its boundaries](./docs/type-safety.md)
|
|
135
|
+
|
|
136
|
+
## What you get
|
|
137
|
+
|
|
138
|
+
| Capability | Practical use |
|
|
139
|
+
| --- | --- |
|
|
140
|
+
| Accumulated registration types | Resolve registered literal tokens with inferred return types |
|
|
141
|
+
| Scope-aware factory resolvers | Detect direct access to scoped dependencies from singleton/transient factories |
|
|
142
|
+
| Async type tracking | Keep `Promise` results visible to the compiler |
|
|
143
|
+
| Singleton, transient and scoped lifetimes | Share infrastructure and isolate request state |
|
|
144
|
+
| Explicit factories and `use()` composition | Group registrations and replace infrastructure in tests |
|
|
145
|
+
| Class, string, number and symbol tokens | Choose the token style that fits your application |
|
|
146
|
+
| Optional and multiple resolution | Use `tryResolve`, `resolveAll` and `tryResolveAll` |
|
|
147
|
+
| Resource cleanup and lazy resolution | Opt in through `katagami/disposable` and `katagami/lazy` |
|
|
148
|
+
| ESM, CommonJS and zero runtime dependencies | Use standard tooling without decorator metadata setup |
|
|
149
|
+
|
|
150
|
+
Use Katagami when dependency wiring, test substitution or request lifetimes need structure.
|
|
151
|
+
For a few dependencies, ordinary constructor/function parameters may be enough.
|
|
152
|
+
See [choosing a DI approach](./docs/choosing-di.md) for trade-offs and links to alternatives.
|
|
153
|
+
|
|
154
|
+
## Documentation
|
|
155
|
+
|
|
156
|
+
- [Usage guide and API](./docs/guide.md): lifetimes, composition, classes, async factories, cleanup and lazy resolution.
|
|
157
|
+
- [Type guarantees](./docs/type-safety.md): accumulated tokens, interface maps and structural class typing.
|
|
158
|
+
- [AI coding guide](./docs/ai-coding-agents.md): workflow, diagnostics and a prompt to use in your project.
|
|
159
|
+
- [Request-scope starter](./examples/request-scope/README.md): concurrent requests, fake repositories and cleanup.
|
|
160
|
+
- [AI-assisted dependency wiring](./docs/articles/ai-coding-agents.md): a worked example.
|
|
161
|
+
- [Request-scope mistakes](./docs/articles/request-scope.md): identifying and fixing a captive dependency.
|
|
162
|
+
- [DI without decorators](./docs/articles/without-decorators.md): installation and test substitution.
|
|
163
|
+
- [Contributing](https://github.com/hiroiku/katagami/blob/master/CONTRIBUTING.md) and [changelog](https://github.com/hiroiku/katagami/blob/master/CHANGELOG.md).
|
|
164
|
+
|
|
165
|
+
TypeScript examples use strict type checking. Core DI needs no polyfills. Optional resource
|
|
166
|
+
cleanup requires the host's disposal symbols; `await using` also requires suitable TypeScript
|
|
167
|
+
compiler/lib settings. See the [compatibility notes](./docs/guide.md#compatibility).
|
|
168
|
+
|
|
169
|
+
## About the name
|
|
170
|
+
|
|
171
|
+
型紙 (*katagami*) is stencil paper used in traditional Japanese dyeing.
|
|
172
|
+
Layered stencils compose a pattern, just as registrations accumulate types in a method chain.
|
|
617
173
|
|
|
618
174
|
## License
|
|
619
175
|
|