katagami 2.3.0 → 3.0.1

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.
Files changed (50) hide show
  1. package/README.md +82 -592
  2. package/dist/chunk-J2NYR3SH.js +6 -0
  3. package/dist/container/index.d.cts +108 -0
  4. package/dist/container/index.d.ts +5 -82
  5. package/dist/disposable/index.cjs +16 -25
  6. package/dist/disposable/index.d.cts +69 -0
  7. package/dist/disposable/index.d.ts +16 -8
  8. package/dist/disposable/index.js +1 -1
  9. package/dist/error/index.d.cts +11 -0
  10. package/dist/index.cjs +181 -86
  11. package/dist/index.d.cts +6 -0
  12. package/dist/index.d.ts +6 -6
  13. package/dist/index.js +177 -45
  14. package/dist/internal.d.cts +29 -0
  15. package/dist/internal.d.ts +3 -1
  16. package/dist/lazy/index.cjs +16 -25
  17. package/dist/lazy/index.d.cts +33 -0
  18. package/dist/lazy/index.d.ts +7 -9
  19. package/dist/lazy/index.js +1 -1
  20. package/dist/resolver/index.d.cts +93 -0
  21. package/dist/scope/index.d.cts +120 -0
  22. package/dist/scope/index.d.ts +4 -4
  23. package/docs/README.de.md +65 -0
  24. package/docs/README.es.md +65 -0
  25. package/docs/README.fr.md +65 -0
  26. package/docs/README.ja.md +66 -0
  27. package/docs/README.ko.md +65 -0
  28. package/docs/README.zh-CN.md +65 -0
  29. package/docs/README.zh-TW.md +65 -0
  30. package/docs/ai-coding-agents.md +78 -0
  31. package/docs/articles/ai-coding-agents.ja.md +83 -0
  32. package/docs/articles/ai-coding-agents.md +70 -0
  33. package/docs/articles/request-scope.md +48 -0
  34. package/docs/articles/without-decorators.md +54 -0
  35. package/docs/choosing-di.md +30 -0
  36. package/docs/growth/baseline-2026-09-11.json +68 -0
  37. package/docs/growth/github-metadata.json +13 -0
  38. package/docs/growth/rollout.md +77 -0
  39. package/docs/guide.md +186 -0
  40. package/docs/type-safety.md +126 -0
  41. package/examples/request-scope/README.md +37 -0
  42. package/examples/request-scope/app.ts +31 -0
  43. package/examples/request-scope/demo.ts +10 -0
  44. package/examples/request-scope/tsconfig.json +11 -0
  45. package/llms.txt +16 -0
  46. package/package.json +56 -28
  47. package/dist/index-g50fxds1.js +0 -34
  48. package/dist/index-jx8b52m0.js +0 -4
  49. package/dist/scope/index.cjs +0 -212
  50. package/dist/scope/index.js +0 -153
package/README.md CHANGED
@@ -2,626 +2,116 @@
2
2
 
3
3
  # Katagami
4
4
 
5
- Lightweight TypeScript DI container with full type inference.
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
  [![npm version](https://img.shields.io/npm/v/katagami)](https://www.npmjs.com/package/katagami)
12
+ [![CI](https://github.com/hiroiku/katagami/actions/workflows/ci.yml/badge.svg)](https://github.com/hiroiku/katagami/actions/workflows/ci.yml)
8
13
  [![license](https://img.shields.io/npm/l/katagami)](https://github.com/hiroiku/katagami/blob/master/LICENSE)
9
- [![bundle size](https://img.shields.io/bundlephobia/minzip/katagami)](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/scope`, `katagami/disposable`) 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
14
 
31
- ## Install
32
-
33
- ```bash
15
+ ```sh
34
16
  npm install katagami
35
17
  ```
36
18
 
37
- ## Quick Start
38
-
39
- ```ts
40
- import { createContainer } 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 = 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
-
97
- Katagami is split into subpath exports. Import only what you use — `katagami/scope`, `katagami/disposable`, and `katagami/lazy` are completely eliminated from the bundle if not imported. Combined with `sideEffects: false`, bundlers can remove every unused byte.
98
-
99
- ```ts
100
- // Core only — scope, disposable, and lazy are not included in the bundle
101
- import { createContainer } from 'katagami';
102
-
103
- // Import only what you need
104
- import { createScope } from 'katagami/scope';
105
- import { disposable } from 'katagami/disposable';
106
- import { lazy } from 'katagami/lazy';
107
- ```
108
-
109
- ### Full type inference from class tokens
110
-
111
- 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.
112
-
113
- ### Method-chain type accumulation
114
-
115
- 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.
116
-
117
- ### Hybrid token strategy
118
-
119
- 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.
120
-
121
- ### Zero dependencies
122
-
123
- No runtime dependencies, no polyfills. No need to add reflect-metadata (~50 KB unminified) to your bundle.
124
-
125
- ## Guide
126
-
127
- ### Singleton & Transient
128
-
129
- Singleton creates the instance on the first `resolve` and caches it. Transient creates a new instance every time.
19
+ ## Quick start
130
20
 
131
21
  ```ts
132
- import { createContainer } from 'katagami';
133
-
134
- class Database {
135
- constructor(public id = Math.random()) {}
136
- }
137
-
138
- class RequestHandler {
139
- constructor(public id = Math.random()) {}
140
- }
22
+ import { createContainer, createScope } from 'katagami';
141
23
 
142
24
  const container = createContainer()
143
- .registerSingleton(Database, () => new Database())
144
- .registerTransient(RequestHandler, () => new RequestHandler());
145
-
146
- // Singleton same instance every time
147
- container.resolve(Database) === container.resolve(Database); // true
148
-
149
- // Transient — new instance every time
150
- container.resolve(RequestHandler) === container.resolve(RequestHandler); // false
151
- ```
152
-
153
- ### Scoped Lifetime & Child Containers
154
-
155
- Scoped registrations behave like singletons within a scope but produce a fresh instance in each new scope. Import `createScope` from `katagami/scope` to create a child container. Scoped tokens cannot be resolved from the root container.
156
-
157
- ```ts
158
- import { createContainer } from 'katagami';
159
- import { createScope } from 'katagami/scope';
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
183
- ```
184
-
185
- Scopes can also be nested. Each nested scope has its own scoped instance cache while sharing singletons with its parent:
186
-
187
- ```ts
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
- ```
197
-
198
- ### Module Composition
199
-
200
- Group related registrations into a reusable module by creating a container with `createContainer()`, then apply it to another container with `use()`. Only registration entries are copied — singleton instance caches are not shared.
201
-
202
- ```ts
203
- import { createContainer } from 'katagami';
204
-
205
- class AuthService {
206
- authenticate() {
207
- return true;
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 } 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 logger = container.resolve(Logger);
270
- // ^? Logger
271
-
272
- const db = await container.resolve(Database);
273
- // ^? Promise<Database> (awaited → Database)
274
- db.connected; // true
275
- ```
276
-
277
- Async factories can depend on both sync and async registrations:
278
-
279
- ```ts
280
- const container = createContainer()
281
- .registerSingleton(Logger, () => new Logger())
282
- .registerSingleton(Database, async r => {
283
- const logger = r.resolve(Logger); // sync → Logger
284
- logger.log('Connecting...');
285
- return new Database(true);
286
- });
287
- ```
288
-
289
- ### Circular Dependency Detection
290
-
291
- 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:
292
-
293
- ```ts
294
- import { createContainer } from 'katagami';
295
-
296
- class ServiceA {
297
- constructor(public b: ServiceB) {}
298
- }
299
-
300
- class ServiceB {
301
- constructor(public a: ServiceA) {}
302
- }
303
-
304
- const container = createContainer()
305
- .registerSingleton(ServiceA, r => new ServiceA(r.resolve(ServiceB)))
306
- .registerSingleton(ServiceB, r => new ServiceB(r.resolve(ServiceA)));
307
-
308
- container.resolve(ServiceA);
309
- // ContainerError: Circular dependency detected: ServiceA -> ServiceB -> ServiceA
310
- ```
311
-
312
- Indirect cycles are also detected:
313
-
314
- ```
315
- ContainerError: Circular dependency detected: ServiceX -> ServiceY -> ServiceZ -> ServiceX
316
- ```
317
-
318
- ### Disposable Support
319
-
320
- 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.
321
-
322
- ```ts
323
- import { createContainer } from 'katagami';
324
- import { disposable } from 'katagami/disposable';
325
-
326
- class Connection {
327
- async [Symbol.asyncDispose]() {
328
- console.log('Connection closed');
329
- }
330
- }
331
-
332
- // Manual disposal
333
- const container = disposable(createContainer().registerSingleton(Connection, () => new Connection()));
334
-
335
- container.resolve(Connection);
336
- await container[Symbol.asyncDispose]();
337
- // => "Connection closed"
338
- ```
339
-
340
- With `await using`, scopes are automatically disposed at the end of the block:
341
-
342
- ```ts
343
- import { createScope } from 'katagami/scope';
344
- import { disposable } from 'katagami/disposable';
345
-
346
- const root = createContainer()
347
- .registerSingleton(DbPool, () => new DbPool())
348
- .registerScoped(Connection, () => new Connection());
349
-
350
- {
351
- await using scope = disposable(createScope(root));
352
- const conn = scope.resolve(Connection);
353
- // ... use conn ...
354
- } // scope is disposed here — Connection is cleaned up, DbPool is not
355
- ```
356
-
357
- Scope disposal only affects scoped instances. Singleton instances are owned by the root container and are disposed when the container itself is disposed.
358
-
359
- 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:
360
-
361
- ```ts
362
- const container = disposable(createContainer().registerSingleton(Connection, () => new Connection()));
363
-
364
- container.resolve(Connection); // OK
365
- container.registerSingleton(/* ... */); // Compile-time error
366
- ```
367
-
368
- ### Lazy Resolution
369
-
370
- 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.
371
-
372
- ```ts
373
- import { createContainer } from 'katagami';
374
- import { lazy } from 'katagami/lazy';
375
-
376
- class HeavyService {
377
- constructor() {
378
- // expensive initialization
379
- }
380
- process() {
381
- return 'done';
382
- }
383
- }
384
-
385
- const container = createContainer().registerSingleton(HeavyService, () => new HeavyService());
386
-
387
- const service = lazy(container, HeavyService);
388
- // HeavyService is NOT instantiated yet
389
-
390
- service.process(); // instance created here, then cached
391
- service.process(); // uses the cached instance
392
- ```
393
-
394
- 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.
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
+ });
395
30
 
396
- Only **sync class tokens** are supported. Async tokens and PropertyKey tokens are rejected at the type level because Proxy traps are synchronous.
397
-
398
- `lazy()` works with Container, Scope, DisposableContainer, and DisposableScope:
399
-
400
- ```ts
401
- import { createScope } from 'katagami/scope';
402
-
403
- const root = createContainer().registerScoped(RequestContext, () => new RequestContext());
404
- const scope = createScope(root);
405
-
406
- const ctx = lazy(scope, RequestContext); // deferred scoped resolution
31
+ const scope = createScope(container);
32
+ scope.resolve('greeting')('world');
33
+ // scope.resolve('missing'); // Type error: this token has not been registered
407
34
  ```
408
35
 
409
- ### Tree Shaking
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.
410
39
 
411
- Katagami uses subpath exports to split functionality into independent entry points. If you only need the core container, `katagami/scope`, `katagami/disposable`, and `katagami/lazy` are completely excluded from the bundle. The package declares `sideEffects: false`, so bundlers can safely eliminate any unused code.
40
+ ## Why Katagami for AI-assisted development?
412
41
 
413
- ```ts
414
- // Core only scope, disposable, and lazy are not included in the bundle
415
- import { createContainer } from 'katagami';
42
+ Give coding agents a concrete feedback loop: edit dependency wiring, run the TypeScript checker,
43
+ and use the diagnostics to fix invalid dependencies. Explicit factories keep dependency edges
44
+ in ordinary TypeScript code that both people and agents can read.
416
45
 
417
- // Import only what you need
418
- import { createScope } from 'katagami/scope';
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:
46
+ For example, a singleton must not capture state that belongs to one request:
426
47
 
427
48
  ```ts
428
49
  import { createContainer } from 'katagami';
429
50
 
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 = container.resolve('greeting');
450
- // ^? string
51
+ createContainer()
52
+ .registerScoped('request', () => ({ id: crypto.randomUUID() }))
53
+ // @ts-expect-error — a singleton factory cannot resolve this scoped token
54
+ .registerSingleton('handler', r => r.resolve('request'));
451
55
  ```
452
56
 
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:
57
+ The compiler reports `No overload matches this call` at `r.resolve('request')`.
58
+ The factory's resolver does not include scoped tokens. Give the handler the same lifetime as the request:
470
59
 
471
60
  ```ts
472
61
  import { createContainer } from 'katagami';
473
62
 
474
- class DbPool {}
475
- class RequestContext {}
476
-
477
- const container = createContainer()
478
- .registerScoped(RequestContext, () => new RequestContext())
479
- // @ts-expect-error — singleton factory cannot resolve scoped token
480
- .registerSingleton(DbPool, r => new DbPool(r.resolve(RequestContext)));
481
- ```
482
-
483
- Scoped factories, on the other hand, can resolve both scoped and non-scoped tokens:
484
-
485
- ```ts
486
63
  const container = createContainer()
487
- .registerSingleton(DbPool, () => new DbPool())
488
- .registerScoped(RequestContext, r => {
489
- r.resolve(DbPool); // OK — scoped factory can resolve singleton tokens
490
- return new RequestContext();
491
- });
492
- ```
493
-
494
- Katagami also enforces this rule at runtime within scopes. If a singleton factory attempts to resolve a scoped token — directly or through intermediaries — a `ContainerError` is thrown:
495
-
496
- ```ts
497
- import { createContainer } from 'katagami';
498
- import { createScope } from 'katagami/scope';
499
-
500
- class DbPool {}
501
- class RequestContext {}
502
-
503
- const container = createContainer()
504
- .registerScoped(RequestContext, () => new RequestContext())
505
- .registerSingleton(DbPool, r => new DbPool(r.resolve(RequestContext)));
506
-
507
- const scope = createScope(container);
508
- scope.resolve(DbPool);
509
- // ContainerError: Captive dependency detected: scoped token "RequestContext" cannot be resolved inside a singleton factory.
510
- ```
511
-
512
- ### Optional Resolution (tryResolve)
513
-
514
- When you need to handle optional dependencies or want to check if a token is registered without throwing an error, use `tryResolve`. Unlike `resolve`, it returns `undefined` for unregistered tokens instead of throwing `ContainerError`:
515
-
516
- ```ts
517
- import { createContainer } from 'katagami';
518
-
519
- class Logger {
520
- log(msg: string) {
521
- console.log(msg);
522
- }
523
- }
524
-
525
- class Analytics {
526
- track(event: string) {
527
- console.log(`Track: ${event}`);
528
- }
529
- }
530
-
531
- const container = createContainer().registerSingleton(Logger, () => new Logger());
532
-
533
- // resolve throws for unregistered tokens
534
- container.resolve(Analytics); // ContainerError: Token "Analytics" is not registered.
535
-
536
- // tryResolve returns undefined for unregistered tokens
537
- const analytics = container.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
- ### `Container.prototype.resolve(token)`
587
-
588
- Resolves and returns the instance for the given token. Throws `ContainerError` if the token is not registered or if a circular dependency is detected.
589
-
590
- ### `Container.prototype.tryResolve(token)`
591
-
592
- 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 containers/scopes.
593
-
594
- ### `createScope(source)` — `katagami/scope`
595
-
596
- 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.
597
-
598
- ### `class Scope`
599
-
600
- A scoped child container created by `createScope()`.
601
-
602
- ### `Scope.prototype.resolve(token)`
603
-
604
- Resolves and returns the instance for the given token. Behaves the same as `Container.prototype.resolve`, but can also resolve scoped tokens.
605
-
606
- ### `Scope.prototype.tryResolve(token)`
607
-
608
- 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.
609
-
610
- ### `lazy(source, token)` — `katagami/lazy`
611
-
612
- 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 `Container`, `Scope`, `DisposableContainer`, and `DisposableScope`.
613
-
614
- ### `disposable(container)` — `katagami/disposable`
615
-
616
- 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`, which only expose `resolve` and `tryResolve` — registration methods are excluded at the type level.
617
-
618
- ### `class ContainerError`
619
-
620
- Error class thrown for container failures such as resolving an unregistered token, circular dependencies, or operations on a disposed container/scope.
621
-
622
- ### `type Resolver`
623
-
624
- Type export representing the resolver passed to factory callbacks. Useful when you need to type a function that accepts a resolver parameter.
64
+ .registerScoped('request', () => ({ id: crypto.randomUUID() }))
65
+ .registerScoped('handler', r => r.resolve('request'));
66
+ ```
67
+
68
+ The `@ts-expect-error` above makes the deliberately invalid example a checked regression test.
69
+ In application code, fix the lifetime and run `npx tsc --noEmit`; do not add a suppression.
70
+ These examples prove specific compiler checks. Improvements in agent success rate or token usage
71
+ have not been measured; the [evaluation protocol](https://github.com/hiroiku/katagami/tree/master/benchmarks/agent-wiring)
72
+ describes how to test them.
73
+
74
+ **Start here:** [Guide for AI coding agents](./docs/ai-coding-agents.md) ·
75
+ [Runnable request-scope starter](./examples/request-scope/README.md) ·
76
+ [Type safety and its boundaries](./docs/type-safety.md)
77
+
78
+ ## What you get
79
+
80
+ | Capability | Practical use |
81
+ | --- | --- |
82
+ | Accumulated registration types | Resolve registered literal tokens with inferred return types |
83
+ | Scope-aware factory resolvers | Detect direct access to scoped dependencies from singleton/transient factories |
84
+ | Async type tracking | Keep `Promise` results visible to the compiler |
85
+ | Singleton, transient and scoped lifetimes | Share infrastructure and isolate request state |
86
+ | Explicit factories and `use()` composition | Group registrations and replace infrastructure in tests |
87
+ | Class, string, number and symbol tokens | Choose the token style that fits your application |
88
+ | Optional and multiple resolution | Use `tryResolve`, `resolveAll` and `tryResolveAll` |
89
+ | Resource cleanup and lazy resolution | Opt in through `katagami/disposable` and `katagami/lazy` |
90
+ | ESM, CommonJS and zero runtime dependencies | Use standard tooling without decorator metadata setup |
91
+
92
+ Use Katagami when dependency wiring, test substitution or request lifetimes need structure.
93
+ For a few dependencies, ordinary constructor/function parameters may be enough.
94
+ See [choosing a DI approach](./docs/choosing-di.md) for trade-offs and links to alternatives.
95
+
96
+ ## Documentation
97
+
98
+ - [Usage guide and API](./docs/guide.md): lifetimes, composition, classes, async factories, cleanup and lazy resolution.
99
+ - [Type guarantees](./docs/type-safety.md): accumulated tokens, interface maps and structural class typing.
100
+ - [AI coding guide](./docs/ai-coding-agents.md): workflow, diagnostics and a prompt to use in your project.
101
+ - [Request-scope starter](./examples/request-scope/README.md): concurrent requests, fake repositories and cleanup.
102
+ - [AI-assisted dependency wiring](./docs/articles/ai-coding-agents.md): a worked example.
103
+ - [Request-scope mistakes](./docs/articles/request-scope.md): identifying and fixing a captive dependency.
104
+ - [DI without decorators](./docs/articles/without-decorators.md): installation and test substitution.
105
+ - [Contributing](https://github.com/hiroiku/katagami/blob/master/CONTRIBUTING.md) and [changelog](https://github.com/hiroiku/katagami/blob/master/CHANGELOG.md).
106
+
107
+ TypeScript examples use strict type checking. Core DI needs no polyfills. Optional resource
108
+ cleanup requires the host's disposal symbols; `await using` also requires suitable TypeScript
109
+ compiler/lib settings. See the [compatibility notes](./docs/guide.md#compatibility).
110
+
111
+ ## About the name
112
+
113
+ 型紙 (*katagami*) is stencil paper used in traditional Japanese dyeing.
114
+ Layered stencils compose a pattern, just as registrations accumulate types in a method chain.
625
115
 
626
116
  ## License
627
117