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.
- package/README.md +82 -592
- package/dist/chunk-J2NYR3SH.js +6 -0
- package/dist/container/index.d.cts +108 -0
- package/dist/container/index.d.ts +5 -82
- package/dist/disposable/index.cjs +16 -25
- package/dist/disposable/index.d.cts +69 -0
- package/dist/disposable/index.d.ts +16 -8
- package/dist/disposable/index.js +1 -1
- package/dist/error/index.d.cts +11 -0
- package/dist/index.cjs +181 -86
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +177 -45
- 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 +7 -9
- 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 +65 -0
- package/docs/README.es.md +65 -0
- package/docs/README.fr.md +65 -0
- package/docs/README.ja.md +66 -0
- package/docs/README.ko.md +65 -0
- package/docs/README.zh-CN.md +65 -0
- package/docs/README.zh-TW.md +65 -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 +30 -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 -28
- package/dist/index-g50fxds1.js +0 -34
- package/dist/index-jx8b52m0.js +0 -4
- package/dist/scope/index.cjs +0 -212
- package/dist/scope/index.js +0 -153
package/README.md
CHANGED
|
@@ -2,626 +2,116 @@
|
|
|
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/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
|
-
|
|
32
|
-
|
|
33
|
-
```bash
|
|
15
|
+
```sh
|
|
34
16
|
npm install katagami
|
|
35
17
|
```
|
|
36
18
|
|
|
37
|
-
## Quick
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
+
## Why Katagami for AI-assisted development?
|
|
412
41
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
|