ts-ioc-container 47.4.0 → 48.0.0
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 +260 -145
- package/cjm/container/Container.js +1 -1
- package/cjm/index.js +2 -2
- package/cjm/injector/ProxyInjector.js +5 -12
- package/cjm/injector/inject.js +7 -6
- package/cjm/provider/IProvider.js +1 -13
- package/cjm/provider/Provider.js +2 -2
- package/cjm/registration/Registration.js +2 -2
- package/cjm/token/ClassToken.js +27 -17
- package/cjm/token/FunctionToken.js +25 -15
- package/cjm/token/GroupAliasToken.js +18 -17
- package/cjm/token/InjectionToken.js +5 -0
- package/cjm/token/SingleAliasToken.js +18 -17
- package/cjm/token/SingleToken.js +20 -16
- package/cjm/utils/array.js +0 -11
- package/esm/container/Container.js +1 -1
- package/esm/index.js +2 -2
- package/esm/injector/ProxyInjector.js +5 -12
- package/esm/injector/inject.js +5 -5
- package/esm/provider/IProvider.js +0 -11
- package/esm/provider/Provider.js +2 -2
- package/esm/registration/Registration.js +2 -2
- package/esm/token/ClassToken.js +28 -18
- package/esm/token/FunctionToken.js +25 -15
- package/esm/token/GroupAliasToken.js +19 -18
- package/esm/token/InjectionToken.js +4 -0
- package/esm/token/SingleAliasToken.js +19 -18
- package/esm/token/SingleToken.js +21 -17
- package/esm/utils/array.js +0 -10
- package/package.json +20 -3
- package/typings/hooks/hook.d.ts +1 -1
- package/typings/index.d.ts +2 -2
- package/typings/injector/IInjector.d.ts +1 -2
- package/typings/injector/ProxyInjector.d.ts +1 -1
- package/typings/injector/inject.d.ts +2 -1
- package/typings/provider/IProvider.d.ts +1 -2
- package/typings/provider/Provider.d.ts +1 -1
- package/typings/registration/Registration.d.ts +3 -1
- package/typings/token/ClassToken.d.ts +13 -8
- package/typings/token/FunctionToken.d.ts +10 -7
- package/typings/token/GroupAliasToken.d.ts +13 -8
- package/typings/token/InjectionToken.d.ts +4 -8
- package/typings/token/SingleAliasToken.d.ts +13 -8
- package/typings/token/SingleToken.d.ts +14 -9
- package/typings/utils/array.d.ts +0 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# TypeScript Dependency Injection Container
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|

|
|
@@ -7,51 +7,68 @@
|
|
|
7
7
|

|
|
8
8
|
[](https://github.com/semantic-release/semantic-release)
|
|
9
9
|
|
|
10
|
+
`ts-ioc-container` is a fast, lightweight TypeScript dependency injection
|
|
11
|
+
container for applications that need more than basic constructor injection:
|
|
12
|
+
scoped lifecycles, decorators, typed tokens, lazy dependencies, lifecycle hooks,
|
|
13
|
+
provider pipelines, aliases, and custom injector strategies.
|
|
14
|
+
|
|
15
|
+
Use it as a `tsyringe` alternative when you want a clear API with more flexible
|
|
16
|
+
customization for real application architecture. Use it as an Inversify or
|
|
17
|
+
Awilix alternative when you want a thinner, cleaner API with explicit scopes and
|
|
18
|
+
no global container objects.
|
|
19
|
+
|
|
10
20
|
## Advantages
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
- clean API
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
21
|
+
|
|
22
|
+
- fast TypeScript dependency resolution
|
|
23
|
+
- lightweight and dependency-minimal
|
|
24
|
+
- clean API for classes, keys, tokens, aliases, and scopes
|
|
25
|
+
- no global container object; pass containers and scopes explicitly
|
|
26
|
+
- supports tagged application, request, transaction, page, and widget scopes
|
|
27
|
+
- decorator support with `@register`, `@inject`, `@onConstruct`, and `@onDispose`
|
|
18
28
|
- can [inject properties](#inject-property)
|
|
19
29
|
- can inject [lazy dependencies](#lazy)
|
|
20
|
-
- composable and
|
|
30
|
+
- composable provider and registration pipelines
|
|
31
|
+
- custom injectors, hooks, and provider behavior
|
|
32
|
+
- product behavior covered by executable specs
|
|
21
33
|
|
|
22
34
|
## Content
|
|
23
35
|
|
|
24
36
|
- [Setup](#setup)
|
|
25
37
|
- [Quickstart](#quickstart)
|
|
26
38
|
- [Cheatsheet](#cheatsheet)
|
|
39
|
+
- [Spec-driven workflow](#spec-driven-workflow)
|
|
40
|
+
- [Product capability map](#product-capability-map)
|
|
41
|
+
- [Acceptance specs](#acceptance-specs)
|
|
42
|
+
- [tsyringe alternative](https://igorbabkin.github.io/ts-ioc-container/tsyringe-alternative)
|
|
43
|
+
- [Inversify and Awilix alternative](https://igorbabkin.github.io/ts-ioc-container/inversify-awilix-alternative)
|
|
27
44
|
- [Recipes](#recipes)
|
|
28
45
|
- [Container](#container)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
- [Basic usage](#basic-usage)
|
|
47
|
+
- [Scope](#scope) `tags`
|
|
48
|
+
- [Dynamic Tag Management](#dynamic-tag-management) `addTags`
|
|
49
|
+
- [Instances](#instances)
|
|
50
|
+
- [Dispose](#dispose)
|
|
51
|
+
- [Lazy](#lazy) `lazy`
|
|
52
|
+
- [Lazy with registerPipe](#lazy-with-registerpipe) `lazy()`
|
|
36
53
|
- [Injector](#injector)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
54
|
+
- [Metadata](#metadata) `@inject`
|
|
55
|
+
- [Simple](#simple)
|
|
56
|
+
- [Proxy](#proxy)
|
|
40
57
|
- [Provider](#provider) `provider`
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
- [Singleton](#singleton) `singleton`
|
|
59
|
+
- [Arguments](#arguments) `setArgs` `setArgsFn`
|
|
60
|
+
- [Visibility](#visibility) `visible`
|
|
61
|
+
- [Alias](#alias) `asAlias`
|
|
62
|
+
- [Decorator](#decorator) `decorate`
|
|
46
63
|
- [Registration](#registration) `@register`
|
|
47
|
-
|
|
48
|
-
|
|
64
|
+
- [Key](#key) `asKey`
|
|
65
|
+
- [Scope](#scope) `scope`
|
|
49
66
|
- [Module](#module)
|
|
50
67
|
- [Hook](#hook) `@hook`
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
68
|
+
- [OnConstruct](#onconstruct) `@onConstruct`
|
|
69
|
+
- [OnDispose](#ondispose) `@onDispose`
|
|
70
|
+
- [Inject Property](#inject-property)
|
|
71
|
+
- [Inject Method](#inject-method)
|
|
55
72
|
- [Mock](#mock)
|
|
56
73
|
- [Error](#error)
|
|
57
74
|
|
|
@@ -60,16 +77,19 @@
|
|
|
60
77
|
```shell script
|
|
61
78
|
npm install ts-ioc-container reflect-metadata
|
|
62
79
|
```
|
|
80
|
+
|
|
63
81
|
```shell script
|
|
64
82
|
yarn add ts-ioc-container reflect-metadata
|
|
65
83
|
```
|
|
66
84
|
|
|
67
85
|
Just put it in the entrypoint file of your project. It should be the first line of the code.
|
|
86
|
+
|
|
68
87
|
```typescript
|
|
69
88
|
import 'reflect-metadata';
|
|
70
89
|
```
|
|
71
90
|
|
|
72
91
|
And `tsconfig.json` should have next options:
|
|
92
|
+
|
|
73
93
|
```json
|
|
74
94
|
{
|
|
75
95
|
"compilerOptions": {
|
|
@@ -105,8 +125,9 @@ container.resolve(App).start();
|
|
|
105
125
|
|
|
106
126
|
## Cheatsheet
|
|
107
127
|
|
|
108
|
-
- Register class with key: `@register(bindTo('Key')) class Service {}`
|
|
128
|
+
- Register class with key (preferred): `@register(bindTo('Key')) class Service {}` then `container.addRegistration(R.fromClass(Service))`
|
|
109
129
|
- Register value: `R.fromValue(config).bindToKey('Config')`
|
|
130
|
+
- Register factory: `R.fromFn((c) => createX(c)).bindToKey('X')`
|
|
110
131
|
- Singleton: `@register(singleton())`
|
|
111
132
|
- Scoped registration: `@register(scope((s) => s.hasTag('request')))`
|
|
112
133
|
- Resolve by alias: `container.resolveByAlias('Alias')`
|
|
@@ -115,12 +136,56 @@ container.resolve(App).start();
|
|
|
115
136
|
- Inject decorator: `@inject('Key')`
|
|
116
137
|
- Property inject: `injectProp(target, 'propName', select.token('Key'))`
|
|
117
138
|
|
|
139
|
+
> [!TIP]
|
|
140
|
+
> For classes, prefer the `@register(bindTo('Key'))` decorator over the fluent
|
|
141
|
+
> `R.fromClass(Class).bindToKey('Key')` chain. The decorator co-locates the binding
|
|
142
|
+
> with the class and reads consistently with other registration pipes
|
|
143
|
+
> (`scope`, `singleton`, `setArgsFn`, ...). Use the fluent `bindToKey` chain only
|
|
144
|
+
> for `R.fromValue(...)` and `R.fromFn(...)` (which have no class to decorate)
|
|
145
|
+
> or for third-party classes you don't own.
|
|
146
|
+
|
|
147
|
+
## Spec-driven workflow
|
|
148
|
+
|
|
149
|
+
Public behavior is described as product capabilities before it is implemented.
|
|
150
|
+
The repository keeps the same chain visible in specs, tests, docs, and this
|
|
151
|
+
README:
|
|
152
|
+
|
|
153
|
+
```text
|
|
154
|
+
Business capability -> user story -> acceptance criteria -> executable spec test -> docs and ADRs
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
- ADRs in `docs/adr/` explain durable architecture and process decisions.
|
|
158
|
+
- Specs in `specs/` describe epics, stories, use cases, and acceptance criteria.
|
|
159
|
+
- Acceptance tests in `__tests__/specs/` execute the public contract.
|
|
160
|
+
- README examples in `__tests__/readme/` stay executable and are rendered from `.readme.hbs.md`.
|
|
161
|
+
|
|
162
|
+
### Product capability map
|
|
163
|
+
|
|
164
|
+
| Capability | User outcome | Spec | Acceptance test |
|
|
165
|
+
| ----------------------- | -------------------------------------------------------------------------- | ---------------------------------------- | ------------------------------------------------- |
|
|
166
|
+
| Dependency resolution | Resolve dependencies by key, class, token, or constructor injection. | `specs/epics/dependency-resolution.md` | `__tests__/specs/dependency-resolution.spec.ts` |
|
|
167
|
+
| Scoped lifecycle | Isolate application, request, transaction, page, or widget lifecycles. | `specs/epics/scoped-lifecycle.md` | `__tests__/specs/scoped-lifecycle.spec.ts` |
|
|
168
|
+
| Dependency registration | Describe classes, values, factories, keys, aliases, and scoped services. | `specs/epics/dependency-registration.md` | `__tests__/specs/dependency-registration.spec.ts` |
|
|
169
|
+
| Provider behavior | Cache, decorate, delay, restrict, or parameterize dependency creation. | `specs/epics/provider-behavior.md` | `__tests__/specs/provider-behavior.spec.ts` |
|
|
170
|
+
| Token-based injection | Make dependency requests explicit, typed, reusable, and composable. | `specs/epics/token-based-injection.md` | `__tests__/specs/token-based-injection.spec.ts` |
|
|
171
|
+
| Injector strategies | Support metadata, simple container, proxy, and custom injection styles. | `specs/epics/injector-strategies.md` | `__tests__/specs/injector-strategies.spec.ts` |
|
|
172
|
+
| Lifecycle hooks | Run initialization, cleanup, property injection, and custom hook behavior. | `specs/epics/lifecycle-hooks.md` | `__tests__/specs/lifecycle-hooks.spec.ts` |
|
|
173
|
+
| Container modules | Package container configuration by feature, environment, or lifecycle. | `specs/epics/container-modules.md` | `__tests__/specs/container-modules.spec.ts` |
|
|
174
|
+
| Metadata utilities | Attach labels, tags, and reusable method behavior to application code. | `specs/epics/metadata-utilities.md` | `__tests__/specs/metadata-utilities.spec.ts` |
|
|
175
|
+
| Errors and boundaries | Make misconfiguration and unsupported usage diagnosable. | `specs/epics/errors-and-boundaries.md` | `__tests__/specs/errors-and-boundaries.spec.ts` |
|
|
176
|
+
|
|
177
|
+
### Acceptance specs
|
|
178
|
+
|
|
179
|
+
Use `pnpm run test:spec` to run only the executable acceptance specs. These
|
|
180
|
+
tests are intentionally product-facing; lower-level regression and implementation
|
|
181
|
+
tests stay in the feature folders under `__tests__/`.
|
|
182
|
+
|
|
118
183
|
## Recipes
|
|
119
184
|
|
|
120
185
|
### Express/Next handler (per-request scope)
|
|
186
|
+
|
|
121
187
|
```typescript
|
|
122
|
-
const app = new Container({ tags: ['application'] })
|
|
123
|
-
.addRegistration(R.fromClass(Logger).pipe(singleton()));
|
|
188
|
+
const app = new Container({ tags: ['application'] }).addRegistration(R.fromClass(Logger).pipe(singleton()));
|
|
124
189
|
|
|
125
190
|
function handleRequest() {
|
|
126
191
|
const requestScope = app.createScope({ tags: ['request'] });
|
|
@@ -130,6 +195,7 @@ function handleRequest() {
|
|
|
130
195
|
```
|
|
131
196
|
|
|
132
197
|
### Background worker (singleton client, transient jobs)
|
|
198
|
+
|
|
133
199
|
```typescript
|
|
134
200
|
@register(singleton())
|
|
135
201
|
class QueueClient {}
|
|
@@ -144,10 +210,13 @@ const worker = new Container({ tags: ['worker'] })
|
|
|
144
210
|
```
|
|
145
211
|
|
|
146
212
|
### Frontend widget/page scope with lazy dependency
|
|
213
|
+
|
|
147
214
|
```typescript
|
|
148
215
|
@register(bindTo('FeatureFlags'), singleton())
|
|
149
216
|
class FeatureFlags {
|
|
150
|
-
load() {
|
|
217
|
+
load() {
|
|
218
|
+
/* fetch flags */
|
|
219
|
+
}
|
|
151
220
|
}
|
|
152
221
|
|
|
153
222
|
class Widget {
|
|
@@ -160,6 +229,7 @@ const page = new Container({ tags: ['page'] })
|
|
|
160
229
|
```
|
|
161
230
|
|
|
162
231
|
## Container
|
|
232
|
+
|
|
163
233
|
`IContainer` consists of:
|
|
164
234
|
|
|
165
235
|
- Provider is dependency factory which creates dependency
|
|
@@ -170,7 +240,7 @@ const page = new Container({ tags: ['page'] })
|
|
|
170
240
|
|
|
171
241
|
```typescript
|
|
172
242
|
import 'reflect-metadata';
|
|
173
|
-
import { Container, type IContainer, inject, Registration as R, select } from 'ts-ioc-container';
|
|
243
|
+
import { bindTo, Container, type IContainer, inject, register, Registration as R, select } from 'ts-ioc-container';
|
|
174
244
|
|
|
175
245
|
/**
|
|
176
246
|
* User Management Domain - Basic Dependency Injection
|
|
@@ -193,6 +263,7 @@ describe('Basic usage', function () {
|
|
|
193
263
|
}
|
|
194
264
|
|
|
195
265
|
// Concrete implementation
|
|
266
|
+
@register(bindTo('IUserRepository'))
|
|
196
267
|
class UserRepository implements IUserRepository {
|
|
197
268
|
private users: User[] = [{ id: '1', email: 'admin@example.com', passwordHash: 'hashed_password' }];
|
|
198
269
|
|
|
@@ -213,7 +284,7 @@ describe('Basic usage', function () {
|
|
|
213
284
|
}
|
|
214
285
|
|
|
215
286
|
// Wire up the container
|
|
216
|
-
const container = new Container().addRegistration(R.fromClass(UserRepository)
|
|
287
|
+
const container = new Container().addRegistration(R.fromClass(UserRepository));
|
|
217
288
|
|
|
218
289
|
// Resolve AuthService - UserRepository is automatically injected
|
|
219
290
|
const authService = container.resolve(AuthService);
|
|
@@ -246,11 +317,17 @@ describe('Basic usage', function () {
|
|
|
246
317
|
```
|
|
247
318
|
|
|
248
319
|
### Scope
|
|
320
|
+
|
|
249
321
|
Sometimes you need to create a scope of container. For example, when you want to create a scope per request in web application. You can assign tags to scope and provider and resolve dependencies only from certain scope.
|
|
250
322
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
323
|
+
> [!IMPORTANT]
|
|
324
|
+
> Scope creation is snapshot-like. Existing parent registrations are applied to the child scope when `createScope()` is called, and when a scope doesn't have a dependency it resolves from the parent container.
|
|
325
|
+
|
|
326
|
+
> [!WARNING]
|
|
327
|
+
> Registrations added to a parent after a child scope has already been created are not automatically applied to that existing child. Create a new scope or add the registration to the child explicitly.
|
|
328
|
+
|
|
329
|
+
> [!WARNING]
|
|
330
|
+
> Scope matching happens when a registration is applied to a container. Only registrations whose scope rules match that container are registered there.
|
|
254
331
|
|
|
255
332
|
```typescript
|
|
256
333
|
import 'reflect-metadata';
|
|
@@ -344,13 +421,16 @@ describe('Scopes', function () {
|
|
|
344
421
|
```
|
|
345
422
|
|
|
346
423
|
### Dynamic Tag Management
|
|
424
|
+
|
|
347
425
|
You can dynamically add tags to a container after it's been created using the `addTags()` method. This is useful for environment-based configuration, feature flags, and progressive container setup.
|
|
348
426
|
|
|
349
427
|
- Tags can be added one at a time or multiple at once
|
|
350
|
-
- Tags must be added **before** registrations are applied - scope matching happens at registration time
|
|
351
428
|
- Useful for conditional configuration based on `NODE_ENV` or runtime flags
|
|
352
429
|
- Container can be configured incrementally as the application initializes
|
|
353
430
|
|
|
431
|
+
> [!WARNING]
|
|
432
|
+
> Tags must be added **before** registrations are applied. Scope matching happens at registration time, so adding tags later does not retroactively make providers available.
|
|
433
|
+
|
|
354
434
|
```typescript
|
|
355
435
|
import { bindTo, Container, register, Registration as R, scope } from 'ts-ioc-container';
|
|
356
436
|
|
|
@@ -451,6 +531,7 @@ describe('addTags', () => {
|
|
|
451
531
|
```
|
|
452
532
|
|
|
453
533
|
### Instances
|
|
534
|
+
|
|
454
535
|
Sometimes you want to get all instances from container and its scopes. For example, when you want to dispose all instances of container.
|
|
455
536
|
|
|
456
537
|
- you can get instances from container and scope which were created by injector
|
|
@@ -535,6 +616,7 @@ describe('Instances', function () {
|
|
|
535
616
|
```
|
|
536
617
|
|
|
537
618
|
### Check Registration
|
|
619
|
+
|
|
538
620
|
Sometimes you want to check if a registration with a specific key exists in the container. This is useful for conditional registration logic, validation, and debugging.
|
|
539
621
|
|
|
540
622
|
- `hasRegistration(key)` checks if a registration exists in the current container or parent containers
|
|
@@ -636,15 +718,18 @@ describe('hasRegistration', function () {
|
|
|
636
718
|
```
|
|
637
719
|
|
|
638
720
|
### Dispose
|
|
639
|
-
|
|
721
|
+
|
|
722
|
+
Sometimes you want to dispose a container or scope. For example, when a request, page, widget, or other local lifecycle ends.
|
|
640
723
|
|
|
641
724
|
- container can be disposed
|
|
642
|
-
- when container is disposed then
|
|
643
|
-
|
|
725
|
+
- when container is disposed then it runs its `onDispose` hooks, unregisters its providers, removes its local instances, and detaches from its parent
|
|
726
|
+
|
|
727
|
+
> [!IMPORTANT]
|
|
728
|
+
> Dispose is local to the container being disposed. Child scopes are not disposed automatically; dispose them explicitly when their own lifecycle ends.
|
|
644
729
|
|
|
645
730
|
```typescript
|
|
646
731
|
import 'reflect-metadata';
|
|
647
|
-
import { Container, ContainerDisposedError, Registration as R, select } from 'ts-ioc-container';
|
|
732
|
+
import { bindTo, Container, ContainerDisposedError, register, Registration as R, select } from 'ts-ioc-container';
|
|
648
733
|
|
|
649
734
|
/**
|
|
650
735
|
* User Management Domain - Resource Cleanup
|
|
@@ -663,6 +748,7 @@ import { Container, ContainerDisposedError, Registration as R, select } from 'ts
|
|
|
663
748
|
*/
|
|
664
749
|
|
|
665
750
|
// Simulates a database connection that must be closed
|
|
751
|
+
@register(bindTo('IDatabase'))
|
|
666
752
|
class DatabaseConnection {
|
|
667
753
|
isClosed = false;
|
|
668
754
|
|
|
@@ -680,9 +766,7 @@ class DatabaseConnection {
|
|
|
680
766
|
|
|
681
767
|
describe('Disposing', function () {
|
|
682
768
|
it('should dispose container and prevent further usage', function () {
|
|
683
|
-
const appContainer = new Container({ tags: ['application'] }).addRegistration(
|
|
684
|
-
R.fromClass(DatabaseConnection).bindTo('IDatabase'),
|
|
685
|
-
);
|
|
769
|
+
const appContainer = new Container({ tags: ['application'] }).addRegistration(R.fromClass(DatabaseConnection));
|
|
686
770
|
|
|
687
771
|
// Create a request scope with a database connection
|
|
688
772
|
const requestScope = appContainer.createScope({ tags: ['request'] });
|
|
@@ -705,9 +789,7 @@ describe('Disposing', function () {
|
|
|
705
789
|
});
|
|
706
790
|
|
|
707
791
|
it('should clean up request-scoped resources on request end', function () {
|
|
708
|
-
const appContainer = new Container({ tags: ['application'] }).addRegistration(
|
|
709
|
-
R.fromClass(DatabaseConnection).bindTo('IDatabase'),
|
|
710
|
-
);
|
|
792
|
+
const appContainer = new Container({ tags: ['application'] }).addRegistration(R.fromClass(DatabaseConnection));
|
|
711
793
|
|
|
712
794
|
// Simulate Express.js request lifecycle
|
|
713
795
|
function handleRequest(): { connection: DatabaseConnection; scope: Container } {
|
|
@@ -742,8 +824,17 @@ describe('Disposing', function () {
|
|
|
742
824
|
```
|
|
743
825
|
|
|
744
826
|
### Lazy
|
|
827
|
+
|
|
745
828
|
Sometimes you want to create dependency only when somebody want to invoke it's method or property. This is what `lazy` is for.
|
|
746
829
|
|
|
830
|
+
- Lazy class instances are wrapped in a JavaScript `Proxy`; the real class instance is created on first property or method access.
|
|
831
|
+
|
|
832
|
+
> [!IMPORTANT]
|
|
833
|
+
> `lazy` is designed only for class instances resolved from class providers.
|
|
834
|
+
|
|
835
|
+
> [!WARNING]
|
|
836
|
+
> Do not use `lazy` for primitive values, plain values, functions, or non-class provider results.
|
|
837
|
+
|
|
747
838
|
```typescript
|
|
748
839
|
import 'reflect-metadata';
|
|
749
840
|
import { Container, inject, register, Registration as R, select as s, singleton } from 'ts-ioc-container';
|
|
@@ -869,9 +960,11 @@ describe('lazy provider', () => {
|
|
|
869
960
|
```
|
|
870
961
|
|
|
871
962
|
### Lazy with registerPipe
|
|
872
|
-
|
|
963
|
+
|
|
964
|
+
The `lazy()` registerPipe can be used in two ways: with the `@register` decorator or directly on the `Provider` pipe. This allows you to defer expensive class instance initialization until first access.
|
|
873
965
|
|
|
874
966
|
**Use cases:**
|
|
967
|
+
|
|
875
968
|
- Defer expensive initialization (database connections, SMTP, external APIs)
|
|
876
969
|
- Conditional features that may not be used
|
|
877
970
|
- Breaking circular dependencies
|
|
@@ -884,7 +977,7 @@ The `lazy()` registerPipe can be used in two ways: with the `@register` decorato
|
|
|
884
977
|
|
|
885
978
|
```typescript
|
|
886
979
|
import 'reflect-metadata';
|
|
887
|
-
import { bindTo, Container, inject, lazy, Provider, register, Registration as R, singleton } from 'ts-ioc-container';
|
|
980
|
+
import { args, bindTo, Container, inject, lazy, Provider, register, Registration as R, singleton } from 'ts-ioc-container';
|
|
888
981
|
|
|
889
982
|
/**
|
|
890
983
|
* Lazy Loading with registerPipe
|
|
@@ -1056,13 +1149,9 @@ describe('lazy registerPipe', () => {
|
|
|
1056
1149
|
it('should allow selective lazy loading - email lazy, SMS eager', () => {
|
|
1057
1150
|
const container = new Container()
|
|
1058
1151
|
// EmailService is lazy - won't connect to SMTP until used
|
|
1059
|
-
.addRegistration(
|
|
1060
|
-
R.fromClass(EmailService)
|
|
1061
|
-
.bindToKey('EmailService')
|
|
1062
|
-
.pipe(singleton(), (p) => p.lazy()),
|
|
1063
|
-
)
|
|
1152
|
+
.addRegistration(R.fromClass(EmailService).pipe(singleton(), (p) => p.lazy()))
|
|
1064
1153
|
// SmsService is eager - connects to gateway immediately
|
|
1065
|
-
.addRegistration(R.fromClass(SmsService).
|
|
1154
|
+
.addRegistration(R.fromClass(SmsService).pipe(singleton()))
|
|
1066
1155
|
.addRegistration(R.fromClass(NotificationService));
|
|
1067
1156
|
|
|
1068
1157
|
// Resolve NotificationService
|
|
@@ -1079,12 +1168,8 @@ describe('lazy registerPipe', () => {
|
|
|
1079
1168
|
|
|
1080
1169
|
it('should initialize lazy email service when first accessed', () => {
|
|
1081
1170
|
const container = new Container()
|
|
1082
|
-
.addRegistration(
|
|
1083
|
-
|
|
1084
|
-
.bindToKey('EmailService')
|
|
1085
|
-
.pipe(singleton(), (p) => p.lazy()),
|
|
1086
|
-
)
|
|
1087
|
-
.addRegistration(R.fromClass(SmsService).bindToKey('SmsService').pipe(singleton()))
|
|
1171
|
+
.addRegistration(R.fromClass(EmailService).pipe(singleton(), (p) => p.lazy()))
|
|
1172
|
+
.addRegistration(R.fromClass(SmsService).pipe(singleton()))
|
|
1088
1173
|
.addRegistration(R.fromClass(NotificationService));
|
|
1089
1174
|
|
|
1090
1175
|
const notifications = container.resolve<NotificationService>(NotificationService);
|
|
@@ -1099,16 +1184,8 @@ describe('lazy registerPipe', () => {
|
|
|
1099
1184
|
it('should work with multiple lazy providers', () => {
|
|
1100
1185
|
const container = new Container()
|
|
1101
1186
|
// Both services are lazy
|
|
1102
|
-
.addRegistration(
|
|
1103
|
-
|
|
1104
|
-
.bindToKey('EmailService')
|
|
1105
|
-
.pipe(singleton(), (p) => p.lazy()),
|
|
1106
|
-
)
|
|
1107
|
-
.addRegistration(
|
|
1108
|
-
R.fromClass(SmsService)
|
|
1109
|
-
.bindToKey('SmsService')
|
|
1110
|
-
.pipe(singleton(), (p) => p.lazy()),
|
|
1111
|
-
)
|
|
1187
|
+
.addRegistration(R.fromClass(EmailService).pipe(singleton(), (p) => p.lazy()))
|
|
1188
|
+
.addRegistration(R.fromClass(SmsService).pipe(singleton(), (p) => p.lazy()))
|
|
1112
1189
|
.addRegistration(R.fromClass(NotificationService));
|
|
1113
1190
|
|
|
1114
1191
|
const notifications = container.resolve<NotificationService>(NotificationService);
|
|
@@ -1197,10 +1274,11 @@ describe('lazy registerPipe', () => {
|
|
|
1197
1274
|
* lazy() works seamlessly with other provider transformations.
|
|
1198
1275
|
*/
|
|
1199
1276
|
describe('combining with other pipes', () => {
|
|
1277
|
+
@register(bindTo('Config'))
|
|
1200
1278
|
class ConfigService {
|
|
1201
1279
|
constructor(
|
|
1202
|
-
public apiUrl: string,
|
|
1203
|
-
public timeout: number,
|
|
1280
|
+
@inject(args(0)) public apiUrl: string,
|
|
1281
|
+
@inject(args(1)) public timeout: number,
|
|
1204
1282
|
) {
|
|
1205
1283
|
initLog.push(`ConfigService initialized with ${apiUrl}`);
|
|
1206
1284
|
}
|
|
@@ -1209,7 +1287,6 @@ describe('lazy registerPipe', () => {
|
|
|
1209
1287
|
it('should combine lazy with args and singleton', () => {
|
|
1210
1288
|
const container = new Container().addRegistration(
|
|
1211
1289
|
R.fromClass(ConfigService)
|
|
1212
|
-
.bindToKey('Config')
|
|
1213
1290
|
.pipe(
|
|
1214
1291
|
(p) => p.setArgs(() => ['https://api.example.com', 5000]),
|
|
1215
1292
|
(p) => p.lazy(),
|
|
@@ -1246,6 +1323,7 @@ describe('lazy registerPipe', () => {
|
|
|
1246
1323
|
* - Report generators
|
|
1247
1324
|
*/
|
|
1248
1325
|
describe('real-world example - feature flags', () => {
|
|
1326
|
+
@register(singleton())
|
|
1249
1327
|
class FeatureFlagService {
|
|
1250
1328
|
constructor() {
|
|
1251
1329
|
initLog.push('FeatureFlagService initialized');
|
|
@@ -1285,7 +1363,7 @@ describe('lazy registerPipe', () => {
|
|
|
1285
1363
|
|
|
1286
1364
|
it('should not initialize premium features for standard users', () => {
|
|
1287
1365
|
const container = new Container()
|
|
1288
|
-
.addRegistration(R.fromClass(FeatureFlagService)
|
|
1366
|
+
.addRegistration(R.fromClass(FeatureFlagService))
|
|
1289
1367
|
.addRegistration(R.fromClass(PremiumFeature))
|
|
1290
1368
|
.addRegistration(R.fromClass(Application));
|
|
1291
1369
|
|
|
@@ -1299,7 +1377,7 @@ describe('lazy registerPipe', () => {
|
|
|
1299
1377
|
|
|
1300
1378
|
it('should initialize premium features only for premium users', () => {
|
|
1301
1379
|
const container = new Container()
|
|
1302
|
-
.addRegistration(R.fromClass(FeatureFlagService)
|
|
1380
|
+
.addRegistration(R.fromClass(FeatureFlagService))
|
|
1303
1381
|
.addRegistration(R.fromClass(PremiumFeature))
|
|
1304
1382
|
.addRegistration(R.fromClass(Application));
|
|
1305
1383
|
|
|
@@ -1316,6 +1394,7 @@ describe('lazy registerPipe', () => {
|
|
|
1316
1394
|
```
|
|
1317
1395
|
|
|
1318
1396
|
## Injector
|
|
1397
|
+
|
|
1319
1398
|
`IInjector` is used to describe how dependencies should be injected to constructor.
|
|
1320
1399
|
|
|
1321
1400
|
- `MetadataInjector` - injects dependencies using `@inject` decorator
|
|
@@ -1323,11 +1402,12 @@ describe('lazy registerPipe', () => {
|
|
|
1323
1402
|
- `SimpleInjector` - just passes container to constructor with others arguments
|
|
1324
1403
|
|
|
1325
1404
|
### Metadata
|
|
1405
|
+
|
|
1326
1406
|
This type of injector uses `@inject` decorator to mark where dependencies should be injected. It's bases on `reflect-metadata` package. That's why I call it `MetadataInjector`.
|
|
1327
1407
|
Also you can [inject property.](#inject-property)
|
|
1328
1408
|
|
|
1329
1409
|
```typescript
|
|
1330
|
-
import { Container, inject, Registration as R } from 'ts-ioc-container';
|
|
1410
|
+
import { bindTo, Container, inject, register, Registration as R } from 'ts-ioc-container';
|
|
1331
1411
|
|
|
1332
1412
|
/**
|
|
1333
1413
|
* User Management Domain - Metadata Injection
|
|
@@ -1344,6 +1424,7 @@ import { Container, inject, Registration as R } from 'ts-ioc-container';
|
|
|
1344
1424
|
* Requires: "experimentalDecorators" and "emitDecoratorMetadata" in tsconfig.
|
|
1345
1425
|
*/
|
|
1346
1426
|
|
|
1427
|
+
@register(bindTo('ILogger'))
|
|
1347
1428
|
class Logger {
|
|
1348
1429
|
name = 'Logger';
|
|
1349
1430
|
}
|
|
@@ -1362,9 +1443,7 @@ class App {
|
|
|
1362
1443
|
|
|
1363
1444
|
describe('Metadata Injector', function () {
|
|
1364
1445
|
it('should inject dependencies using @inject decorator', function () {
|
|
1365
|
-
const container = new Container({ tags: ['application'] }).addRegistration(
|
|
1366
|
-
R.fromClass(Logger).bindToKey('ILogger'),
|
|
1367
|
-
);
|
|
1446
|
+
const container = new Container({ tags: ['application'] }).addRegistration(R.fromClass(Logger));
|
|
1368
1447
|
|
|
1369
1448
|
// Container reads @inject metadata and resolves 'ILogger' for the logger parameter
|
|
1370
1449
|
const app = container.resolve(App);
|
|
@@ -1376,10 +1455,11 @@ describe('Metadata Injector', function () {
|
|
|
1376
1455
|
```
|
|
1377
1456
|
|
|
1378
1457
|
### Simple
|
|
1458
|
+
|
|
1379
1459
|
This type of injector just passes container to constructor with others arguments.
|
|
1380
1460
|
|
|
1381
1461
|
```typescript
|
|
1382
|
-
import { Container, type IContainer, Registration as R, SimpleInjector } from 'ts-ioc-container';
|
|
1462
|
+
import { bindTo, Container, type IContainer, register, Registration as R, SimpleInjector } from 'ts-ioc-container';
|
|
1383
1463
|
|
|
1384
1464
|
/**
|
|
1385
1465
|
* Command Pattern - Simple Injector
|
|
@@ -1407,6 +1487,7 @@ class CreateUserCommand implements ICommand {
|
|
|
1407
1487
|
constructor(readonly username: string) {}
|
|
1408
1488
|
}
|
|
1409
1489
|
|
|
1490
|
+
@register(bindTo('HandlerCreateUser'))
|
|
1410
1491
|
class CreateUserHandler implements ICommandHandler {
|
|
1411
1492
|
handle(command: CreateUserCommand): string {
|
|
1412
1493
|
return `User ${command.username} created`;
|
|
@@ -1416,6 +1497,7 @@ class CreateUserHandler implements ICommandHandler {
|
|
|
1416
1497
|
describe('SimpleInjector', function () {
|
|
1417
1498
|
it('should inject container to allow dynamic resolution (Service Locator pattern)', function () {
|
|
1418
1499
|
// Dispatcher needs the container to find handlers dynamically based on command type
|
|
1500
|
+
@register(bindTo('Dispatcher'))
|
|
1419
1501
|
class CommandDispatcher {
|
|
1420
1502
|
constructor(private container: IContainer) {}
|
|
1421
1503
|
|
|
@@ -1428,8 +1510,8 @@ describe('SimpleInjector', function () {
|
|
|
1428
1510
|
}
|
|
1429
1511
|
|
|
1430
1512
|
const container = new Container({ injector: new SimpleInjector() })
|
|
1431
|
-
.addRegistration(R.fromClass(CommandDispatcher)
|
|
1432
|
-
.addRegistration(R.fromClass(CreateUserHandler)
|
|
1513
|
+
.addRegistration(R.fromClass(CommandDispatcher))
|
|
1514
|
+
.addRegistration(R.fromClass(CreateUserHandler));
|
|
1433
1515
|
|
|
1434
1516
|
const dispatcher = container.resolve<CommandDispatcher>('Dispatcher');
|
|
1435
1517
|
const result = dispatcher.dispatch(new CreateUserCommand('alice'));
|
|
@@ -1450,9 +1532,7 @@ describe('SimpleInjector', function () {
|
|
|
1450
1532
|
}
|
|
1451
1533
|
}
|
|
1452
1534
|
|
|
1453
|
-
const container = new Container({ injector: new SimpleInjector() }).addRegistration(
|
|
1454
|
-
R.fromClass(WidgetFactory).bindToKey('WidgetFactory'),
|
|
1455
|
-
);
|
|
1535
|
+
const container = new Container({ injector: new SimpleInjector() }).addRegistration(R.fromClass(WidgetFactory));
|
|
1456
1536
|
|
|
1457
1537
|
// Pass "dark" as the theme argument
|
|
1458
1538
|
const factory = container.resolve<WidgetFactory>('WidgetFactory', { args: ['dark'] });
|
|
@@ -1464,39 +1544,29 @@ describe('SimpleInjector', function () {
|
|
|
1464
1544
|
```
|
|
1465
1545
|
|
|
1466
1546
|
### Proxy
|
|
1547
|
+
|
|
1467
1548
|
This type of injector injects dependencies as dictionary `Record<string, unknown>`.
|
|
1468
1549
|
|
|
1469
|
-
|
|
1470
|
-
|
|
1550
|
+
- **`args` reserved keyword**: accessing `deps.args` returns the raw `args[]` array passed at resolve time
|
|
1551
|
+
- **Alias convention**: any property name containing `"alias"` (case-insensitive) is resolved via `resolveByAlias` instead of `resolve`
|
|
1471
1552
|
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
*
|
|
1475
|
-
* The ProxyInjector injects dependencies as a single object (props/options pattern).
|
|
1476
|
-
* This is popular in modern JavaScript/TypeScript (like React props or destructuring).
|
|
1477
|
-
*
|
|
1478
|
-
* Advantages:
|
|
1479
|
-
* - Named parameters are more readable than positional arguments
|
|
1480
|
-
* - Order of arguments doesn't matter
|
|
1481
|
-
* - Easy to add/remove dependencies without breaking inheritance chains
|
|
1482
|
-
* - Works well with "Clean Architecture" adapters
|
|
1483
|
-
*/
|
|
1553
|
+
```typescript
|
|
1554
|
+
import { bindTo, Container, ProxyInjector, register, Registration as R } from 'ts-ioc-container';
|
|
1484
1555
|
|
|
1485
1556
|
describe('ProxyInjector', function () {
|
|
1486
1557
|
it('should inject dependencies as a props object', function () {
|
|
1558
|
+
@register(bindTo('logger'))
|
|
1487
1559
|
class Logger {
|
|
1488
1560
|
log(msg: string) {
|
|
1489
1561
|
return `Logged: ${msg}`;
|
|
1490
1562
|
}
|
|
1491
1563
|
}
|
|
1492
1564
|
|
|
1493
|
-
// Dependencies defined as an interface
|
|
1494
1565
|
interface UserControllerDeps {
|
|
1495
1566
|
logger: Logger;
|
|
1496
1567
|
prefix: string;
|
|
1497
1568
|
}
|
|
1498
1569
|
|
|
1499
|
-
// Controller receives all dependencies in a single object
|
|
1500
1570
|
class UserController {
|
|
1501
1571
|
private logger: Logger;
|
|
1502
1572
|
private prefix: string;
|
|
@@ -1512,53 +1582,51 @@ describe('ProxyInjector', function () {
|
|
|
1512
1582
|
}
|
|
1513
1583
|
|
|
1514
1584
|
const container = new Container({ injector: new ProxyInjector() })
|
|
1515
|
-
.addRegistration(R.fromClass(Logger)
|
|
1585
|
+
.addRegistration(R.fromClass(Logger))
|
|
1516
1586
|
.addRegistration(R.fromValue('USER:').bindToKey('prefix'))
|
|
1517
|
-
.addRegistration(R.fromClass(UserController)
|
|
1587
|
+
.addRegistration(R.fromClass(UserController));
|
|
1518
1588
|
|
|
1519
1589
|
const controller = container.resolve<UserController>('UserController');
|
|
1520
1590
|
|
|
1521
1591
|
expect(controller.createUser('bob')).toBe('Logged: USER: bob');
|
|
1522
1592
|
});
|
|
1523
1593
|
|
|
1524
|
-
it('should
|
|
1594
|
+
it('should expose runtime args through the reserved "args" property', function () {
|
|
1595
|
+
@register(bindTo('database'))
|
|
1525
1596
|
class Database {}
|
|
1526
1597
|
|
|
1527
|
-
|
|
1598
|
+
class ReportGenerator {
|
|
1528
1599
|
database: Database;
|
|
1529
|
-
format: string;
|
|
1530
|
-
}
|
|
1600
|
+
format: string;
|
|
1531
1601
|
|
|
1532
|
-
|
|
1533
|
-
|
|
1602
|
+
constructor({ database, args }: { database: Database; args: string[] }) {
|
|
1603
|
+
this.database = database;
|
|
1604
|
+
this.format = args[0];
|
|
1605
|
+
}
|
|
1534
1606
|
|
|
1535
1607
|
generate(): string {
|
|
1536
|
-
return `Report in ${this.
|
|
1608
|
+
return `Report in ${this.format}`;
|
|
1537
1609
|
}
|
|
1538
1610
|
}
|
|
1539
1611
|
|
|
1540
1612
|
const container = new Container({ injector: new ProxyInjector() })
|
|
1541
|
-
.addRegistration(R.fromClass(Database)
|
|
1542
|
-
.addRegistration(R.fromClass(ReportGenerator)
|
|
1613
|
+
.addRegistration(R.fromClass(Database))
|
|
1614
|
+
.addRegistration(R.fromClass(ReportGenerator));
|
|
1543
1615
|
|
|
1544
|
-
// "format" is passed at resolution time
|
|
1545
1616
|
const generator = container.resolve<ReportGenerator>('ReportGenerator', {
|
|
1546
|
-
args: [
|
|
1617
|
+
args: ['PDF'],
|
|
1547
1618
|
});
|
|
1548
1619
|
|
|
1549
|
-
expect(generator.
|
|
1620
|
+
expect(generator.database).toBeInstanceOf(Database);
|
|
1550
1621
|
expect(generator.generate()).toBe('Report in PDF');
|
|
1551
1622
|
});
|
|
1552
1623
|
|
|
1553
|
-
it('should resolve
|
|
1554
|
-
// If a property is named "loggersArray", it looks for alias "loggersArray"
|
|
1555
|
-
// and resolves it as an array of all matches.
|
|
1556
|
-
|
|
1624
|
+
it('should resolve dependencies by alias when property name contains "alias"', function () {
|
|
1557
1625
|
class FileLogger {}
|
|
1558
1626
|
class ConsoleLogger {}
|
|
1559
1627
|
|
|
1560
1628
|
interface AppDeps {
|
|
1561
|
-
|
|
1629
|
+
loggersAlias: any[];
|
|
1562
1630
|
}
|
|
1563
1631
|
|
|
1564
1632
|
class App {
|
|
@@ -1567,23 +1635,20 @@ describe('ProxyInjector', function () {
|
|
|
1567
1635
|
|
|
1568
1636
|
const container = new Container({ injector: new ProxyInjector() });
|
|
1569
1637
|
|
|
1570
|
-
// Mocking the behavior for this specific test as ProxyInjector uses resolveByAlias
|
|
1571
|
-
// which delegates to the container.
|
|
1572
|
-
// In a real scenario, you'd register multiple loggers with the same alias.
|
|
1573
1638
|
const mockLoggers = [new FileLogger(), new ConsoleLogger()];
|
|
1574
|
-
|
|
1575
1639
|
container.resolveByAlias = vi.fn().mockReturnValue(mockLoggers);
|
|
1576
1640
|
|
|
1577
1641
|
const app = container.resolve(App);
|
|
1578
1642
|
|
|
1579
|
-
expect(app.deps.
|
|
1580
|
-
expect(container.resolveByAlias).toHaveBeenCalledWith('
|
|
1643
|
+
expect(app.deps.loggersAlias).toBe(mockLoggers);
|
|
1644
|
+
expect(container.resolveByAlias).toHaveBeenCalledWith('loggersAlias');
|
|
1581
1645
|
});
|
|
1582
1646
|
});
|
|
1583
1647
|
|
|
1584
1648
|
```
|
|
1585
1649
|
|
|
1586
1650
|
## Provider
|
|
1651
|
+
|
|
1587
1652
|
Provider is dependency factory which creates dependency.
|
|
1588
1653
|
|
|
1589
1654
|
- `Provider.fromClass(Logger)`
|
|
@@ -1592,10 +1657,12 @@ Provider is dependency factory which creates dependency.
|
|
|
1592
1657
|
|
|
1593
1658
|
```typescript
|
|
1594
1659
|
import {
|
|
1660
|
+
args,
|
|
1595
1661
|
setArgs,
|
|
1596
1662
|
setArgsFn,
|
|
1597
1663
|
bindTo,
|
|
1598
1664
|
Container,
|
|
1665
|
+
inject,
|
|
1599
1666
|
lazy,
|
|
1600
1667
|
Provider,
|
|
1601
1668
|
register,
|
|
@@ -1669,7 +1736,7 @@ describe('Provider', () => {
|
|
|
1669
1736
|
|
|
1670
1737
|
it('supports args decorator for providing extra arguments', () => {
|
|
1671
1738
|
class FileService {
|
|
1672
|
-
constructor(readonly basePath: string) {}
|
|
1739
|
+
constructor(@inject(args(0)) readonly basePath: string) {}
|
|
1673
1740
|
}
|
|
1674
1741
|
|
|
1675
1742
|
const container = new Container().register(
|
|
@@ -1683,7 +1750,7 @@ describe('Provider', () => {
|
|
|
1683
1750
|
|
|
1684
1751
|
it('supports argsFn decorator for dynamic arguments', () => {
|
|
1685
1752
|
class Database {
|
|
1686
|
-
constructor(readonly connectionString: string) {}
|
|
1753
|
+
constructor(@inject(args(0)) readonly connectionString: string) {}
|
|
1687
1754
|
}
|
|
1688
1755
|
|
|
1689
1756
|
const container = new Container().register('DbPath', Provider.fromValue('localhost:5432')).register(
|
|
@@ -1737,10 +1804,13 @@ describe('Provider', () => {
|
|
|
1737
1804
|
```
|
|
1738
1805
|
|
|
1739
1806
|
### Singleton
|
|
1807
|
+
|
|
1740
1808
|
Sometimes you need to create only one instance of dependency per scope. For example, you want to create only one logger per scope.
|
|
1741
1809
|
|
|
1742
1810
|
- Singleton provider creates only one instance in every scope where it's resolved.
|
|
1743
|
-
|
|
1811
|
+
|
|
1812
|
+
> [!IMPORTANT]
|
|
1813
|
+
> Singleton means one instance per scope. If you create a scope `A` of container `root`, then `Logger` of `A` !== `Logger` of `root`.
|
|
1744
1814
|
|
|
1745
1815
|
```typescript
|
|
1746
1816
|
import 'reflect-metadata';
|
|
@@ -1827,17 +1897,39 @@ describe('Singleton', function () {
|
|
|
1827
1897
|
```
|
|
1828
1898
|
|
|
1829
1899
|
### Arguments
|
|
1900
|
+
|
|
1830
1901
|
Sometimes you want to bind some arguments to provider. This is what `ArgsProvider` is for.
|
|
1902
|
+
|
|
1831
1903
|
- `provider(setArgs('someArgument'))`
|
|
1832
1904
|
- `provider(setArgsFn((container) => [container.resolve(Logger), 'someValue']))`
|
|
1833
1905
|
- `Provider.fromClass(Logger).pipe(setArgs('someArgument'))`
|
|
1834
|
-
|
|
1906
|
+
|
|
1907
|
+
### Token as argument
|
|
1908
|
+
|
|
1909
|
+
When you pass an `InjectionToken` via `token.args(...)`, the container resolves it before the value reaches the constructor. Bare constructors are **not** auto-resolved — wrap a class in `ClassToken` to opt into resolution.
|
|
1910
|
+
|
|
1911
|
+
- `ServiceToken.args(ValueToken)` — `ValueToken` is resolved from the container, its value is passed as arg
|
|
1912
|
+
- `ServiceToken.args(new ClassToken(SomeService))` — `SomeService` is constructed by the container
|
|
1913
|
+
- `ServiceToken.args('literal')` — literal value passed directly
|
|
1835
1914
|
|
|
1836
1915
|
### Positional arg injection with `args(index)`
|
|
1837
|
-
|
|
1916
|
+
|
|
1917
|
+
Constructor parameters that should pick up positional args from `ProviderOptions` must be annotated with `@inject(args(index))`. Parameters without `@inject` resolve to `undefined`.
|
|
1918
|
+
|
|
1838
1919
|
- `@inject(args(0))` — resolves the first element of the `args` array passed at resolution time
|
|
1839
1920
|
- Works together with `token.args(...)` to pass typed dependencies through the args context
|
|
1840
1921
|
|
|
1922
|
+
### Immutable token chaining
|
|
1923
|
+
|
|
1924
|
+
`token.args(...)`, `token.argsFn(...)`, and `token.lazy()` all return **new token instances** — the parent token is never mutated. This allows the same token to be specialized in multiple independent ways (one-way linked list: parent → many children).
|
|
1925
|
+
|
|
1926
|
+
```typescript
|
|
1927
|
+
const ApiToken = new SingleToken<IApiClient>('IApiClient');
|
|
1928
|
+
// Independent children — ApiToken is unchanged
|
|
1929
|
+
const dataToken = ApiToken.args('https://data.api.com', 5000);
|
|
1930
|
+
const userToken = ApiToken.args('https://users.api.com', 1000);
|
|
1931
|
+
```
|
|
1932
|
+
|
|
1841
1933
|
```typescript
|
|
1842
1934
|
import {
|
|
1843
1935
|
setArgs,
|
|
@@ -1845,10 +1937,10 @@ import {
|
|
|
1845
1937
|
bindTo,
|
|
1846
1938
|
Container,
|
|
1847
1939
|
inject,
|
|
1940
|
+
args,
|
|
1848
1941
|
MultiCache,
|
|
1849
1942
|
register,
|
|
1850
1943
|
Registration as R,
|
|
1851
|
-
resolveByArgs,
|
|
1852
1944
|
singleton,
|
|
1853
1945
|
SingleToken,
|
|
1854
1946
|
} from 'ts-ioc-container';
|
|
@@ -1871,7 +1963,7 @@ describe('ArgsProvider', function () {
|
|
|
1871
1963
|
describe('Static Arguments', () => {
|
|
1872
1964
|
it('can pass static arguments to constructor', function () {
|
|
1873
1965
|
class FileLogger {
|
|
1874
|
-
constructor(public filename: string) {}
|
|
1966
|
+
constructor(@inject(args(0)) public filename: string) {}
|
|
1875
1967
|
}
|
|
1876
1968
|
|
|
1877
1969
|
// Pre-configure the logger with a filename
|
|
@@ -1884,7 +1976,7 @@ describe('ArgsProvider', function () {
|
|
|
1884
1976
|
|
|
1885
1977
|
it('prioritizes provided args over resolve args', function () {
|
|
1886
1978
|
class Logger {
|
|
1887
|
-
constructor(public context: string) {}
|
|
1979
|
+
constructor(@inject(args(0)) public context: string) {}
|
|
1888
1980
|
}
|
|
1889
1981
|
|
|
1890
1982
|
// 'FixedContext' wins over any runtime args
|
|
@@ -1905,7 +1997,7 @@ describe('ArgsProvider', function () {
|
|
|
1905
1997
|
}
|
|
1906
1998
|
|
|
1907
1999
|
class Service {
|
|
1908
|
-
constructor(public env: string) {}
|
|
2000
|
+
constructor(@inject(args(0)) public env: string) {}
|
|
1909
2001
|
}
|
|
1910
2002
|
|
|
1911
2003
|
const root = createContainer()
|
|
@@ -1945,17 +2037,18 @@ describe('ArgsProvider', function () {
|
|
|
1945
2037
|
name = 'TodoRepository';
|
|
1946
2038
|
}
|
|
1947
2039
|
|
|
1948
|
-
// EntityManager is generic - it works with ANY repository
|
|
1949
|
-
//
|
|
2040
|
+
// EntityManager is generic - it works with ANY repository.
|
|
2041
|
+
// The repository is the first arg passed via `EntityManagerToken.args(...)`.
|
|
2042
|
+
// `@inject(args(0))` reads it; the container auto-resolves InjectionToken args
|
|
2043
|
+
// before they reach the constructor.
|
|
1950
2044
|
const EntityManagerToken = new SingleToken<EntityManager>('EntityManager');
|
|
1951
2045
|
|
|
1952
2046
|
@register(
|
|
1953
2047
|
bindTo(EntityManagerToken),
|
|
1954
|
-
setArgsFn(resolveByArgs), // <--- Key magic: resolves dependencies based on arguments passed to token
|
|
1955
2048
|
singleton(MultiCache.fromFirstArg), // Cache unique instance per repository type
|
|
1956
2049
|
)
|
|
1957
2050
|
class EntityManager {
|
|
1958
|
-
constructor(public repository: IRepository) {}
|
|
2051
|
+
constructor(@inject(args(0)) public repository: IRepository) {}
|
|
1959
2052
|
}
|
|
1960
2053
|
|
|
1961
2054
|
class App {
|
|
@@ -2007,7 +2100,12 @@ describe('ArgsProvider', function () {
|
|
|
2007
2100
|
```
|
|
2008
2101
|
|
|
2009
2102
|
### Visibility
|
|
2103
|
+
|
|
2010
2104
|
Sometimes you want to hide dependency if somebody wants to resolve it from certain scope. This uses `ScopeAccessRule` to control access.
|
|
2105
|
+
|
|
2106
|
+
> [!IMPORTANT]
|
|
2107
|
+
> Use `scope()` to decide where a provider is registered. Use `scopeAccess()` to decide which invocation scopes can see an already registered provider.
|
|
2108
|
+
|
|
2011
2109
|
- `provider(scopeAccess(({ invocationScope, providerScope }) => invocationScope === providerScope))` - dependency will be accessible only from the scope where it's registered
|
|
2012
2110
|
- `Provider.fromClass(Logger).pipe(scopeAccess(({ invocationScope, providerScope }) => invocationScope === providerScope))`
|
|
2013
2111
|
|
|
@@ -2103,7 +2201,9 @@ describe('Visibility', function () {
|
|
|
2103
2201
|
```
|
|
2104
2202
|
|
|
2105
2203
|
### Alias
|
|
2204
|
+
|
|
2106
2205
|
Alias is needed to group keys
|
|
2206
|
+
|
|
2107
2207
|
- `@register(asAlias('logger'))` helper assigns `logger` alias to registration.
|
|
2108
2208
|
- `by.aliases((it) => it.has('logger') || it.has('a'))` resolves dependencies which have `logger` or `a` aliases
|
|
2109
2209
|
- `Provider.fromClass(Logger).pipe(alias('logger'))`
|
|
@@ -2248,11 +2348,14 @@ describe('alias', () => {
|
|
|
2248
2348
|
```
|
|
2249
2349
|
|
|
2250
2350
|
### Decorator
|
|
2351
|
+
|
|
2251
2352
|
Sometimes you want to decorate you class with some logic. This is what `DecoratorProvider` is for.
|
|
2353
|
+
|
|
2252
2354
|
- `provider(decorate((instance, container) => new LoggerDecorator(instance)))`
|
|
2253
2355
|
|
|
2254
2356
|
```typescript
|
|
2255
2357
|
import {
|
|
2358
|
+
args,
|
|
2256
2359
|
bindTo,
|
|
2257
2360
|
Container,
|
|
2258
2361
|
decorate,
|
|
@@ -2306,7 +2409,7 @@ describe('Decorator Pattern', () => {
|
|
|
2306
2409
|
// Decorator: Wraps any IRepository with logging behavior
|
|
2307
2410
|
class LoggingRepository implements IRepository {
|
|
2308
2411
|
constructor(
|
|
2309
|
-
private repository: IRepository,
|
|
2412
|
+
@inject(args(0)) private repository: IRepository,
|
|
2310
2413
|
@inject(s.token('Logger').lazy()) private logger: Logger,
|
|
2311
2414
|
) {}
|
|
2312
2415
|
|
|
@@ -2362,7 +2465,9 @@ describe('Decorator Pattern', () => {
|
|
|
2362
2465
|
```
|
|
2363
2466
|
|
|
2364
2467
|
## Registration
|
|
2468
|
+
|
|
2365
2469
|
Registration is provider factory which registers provider in container.
|
|
2470
|
+
|
|
2366
2471
|
- `@register(asKey('logger'))`
|
|
2367
2472
|
- `Registration.fromClass(Logger).to('logger')`
|
|
2368
2473
|
- `Registration.fromClass(Logger)`
|
|
@@ -2370,6 +2475,7 @@ Registration is provider factory which registers provider in container.
|
|
|
2370
2475
|
- `Registration.fromFn((container, ...args) => container.resolve(Logger, {args}))`
|
|
2371
2476
|
|
|
2372
2477
|
### Key
|
|
2478
|
+
|
|
2373
2479
|
Sometimes you want to register provider with certain key. This is what `key` is for.
|
|
2374
2480
|
|
|
2375
2481
|
- by default, key is class name
|
|
@@ -2462,7 +2568,9 @@ describe('Registration module', function () {
|
|
|
2462
2568
|
```
|
|
2463
2569
|
|
|
2464
2570
|
### Scope
|
|
2571
|
+
|
|
2465
2572
|
Sometimes you need to register provider only in scope which matches to certain condition and their sub scopes. Especially if you want to register dependency as singleton for some tags, for example `root`. This uses `ScopeMatchRule` to determine which scopes should have the provider.
|
|
2573
|
+
|
|
2466
2574
|
- `@register(scope((container) => container.hasTag('root'))` - register provider only in root scope
|
|
2467
2575
|
- `Registration.fromClass(Logger).when((container) => container.hasTag('root'))`
|
|
2468
2576
|
|
|
@@ -2504,6 +2612,7 @@ describe('ScopeProvider', function () {
|
|
|
2504
2612
|
```
|
|
2505
2613
|
|
|
2506
2614
|
## Module
|
|
2615
|
+
|
|
2507
2616
|
Sometimes you want to encapsulate registration logic in separate module. This is what `IContainerModule` is for.
|
|
2508
2617
|
|
|
2509
2618
|
```typescript
|
|
@@ -2627,9 +2736,11 @@ describe('Container Modules', function () {
|
|
|
2627
2736
|
```
|
|
2628
2737
|
|
|
2629
2738
|
## Hook
|
|
2739
|
+
|
|
2630
2740
|
Sometimes you need to invoke methods after construct or dispose of class. This is what hooks are for.
|
|
2631
2741
|
|
|
2632
2742
|
### OnConstruct
|
|
2743
|
+
|
|
2633
2744
|
```typescript
|
|
2634
2745
|
import {
|
|
2635
2746
|
AddOnConstructHookModule,
|
|
@@ -2690,6 +2801,7 @@ describe('onConstruct', function () {
|
|
|
2690
2801
|
```
|
|
2691
2802
|
|
|
2692
2803
|
### OnDispose
|
|
2804
|
+
|
|
2693
2805
|
```typescript
|
|
2694
2806
|
import {
|
|
2695
2807
|
AddOnDisposeHookModule,
|
|
@@ -2804,8 +2916,11 @@ describe('inject property', () => {
|
|
|
2804
2916
|
|
|
2805
2917
|
## Error
|
|
2806
2918
|
|
|
2919
|
+
The product-facing error contract is described in
|
|
2920
|
+
`specs/epics/errors-and-boundaries.md` and executed by
|
|
2921
|
+
`__tests__/specs/errors-and-boundaries.spec.ts`.
|
|
2922
|
+
|
|
2807
2923
|
- [DependencyNotFoundError.ts](..%2F..%2Flib%2Ferrors%2FDependencyNotFoundError.ts)
|
|
2808
2924
|
- [MethodNotImplementedError.ts](..%2F..%2Flib%2Ferrors%2FMethodNotImplementedError.ts)
|
|
2809
2925
|
- [DependencyMissingKeyError.ts](..%2F..%2Flib%2Ferrors%2FDependencyMissingKeyError.ts)
|
|
2810
2926
|
- [ContainerDisposedError.ts](..%2F..%2Flib%2Ferrors%2FContainerDisposedError.ts)
|
|
2811
|
-
|