├── .eslintrc.yml ├── .gitbook.yaml ├── .github ├── dependabot.yml ├── semantic.yml └── workflows │ ├── auto-merge-dependabot-workflow.yml │ ├── continuous-deployment-workflow.yml │ ├── continuous-integration-workflow.yml │ └── lock-closed-issues-workflow.yml ├── .gitignore ├── .prettierrc.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── codecov.yml ├── docs ├── README.md ├── SUMMARY.md ├── javascript │ ├── 01-getting-started.md │ └── 02-basic-usage.md └── typescript │ ├── 01-getting-started.md │ ├── 02-basic-usage-guide.md │ ├── 03-container-api.md │ ├── 04-service-decorator.md │ ├── 05-inject-decorator.md │ ├── 06-service-tokens.md │ ├── 07-inheritance.md │ ├── 07-usage-with-typeorm.md │ ├── 08-custom-decorators.md │ ├── 09-using-scoped-containers.md │ └── 10-using-transient-services.md ├── jest.config.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── container-instance.class.ts ├── container-registry.class.ts ├── decorators │ ├── inject-many.decorator.ts │ ├── inject.decorator.ts │ └── service.decorator.ts ├── empty.const.ts ├── error │ ├── cannot-inject-value.error.ts │ ├── cannot-instantiate-value.error.ts │ └── service-not-found.error.ts ├── index.ts ├── interfaces │ ├── container-options.interface.ts │ ├── handler.interface.ts │ ├── service-metadata.interface.ts │ └── service-options.interface.ts ├── token.class.ts ├── types │ ├── abstract-constructable.type.ts │ ├── constructable.type.ts │ ├── container-identifier.type.ts │ ├── container-scope.type.ts │ └── service-identifier.type.ts └── utils │ └── resolve-to-type-wrapper.util.ts ├── test ├── Container.spec.ts ├── decorators │ ├── Inject.spec.ts │ └── Service.spec.ts ├── eager-loading-services.spec.ts ├── github-issues │ ├── 40 │ │ └── issue-40.spec.ts │ ├── 41 │ │ └── issue-41.spec.ts │ ├── 42 │ │ └── issue-42.spec.ts │ ├── 48 │ │ └── issue-48.spec.ts │ ├── 53 │ │ └── issue-53.spec.ts │ ├── 56 │ │ └── issue-56.spec.ts │ ├── 61 │ │ └── issue-61.spec.ts │ ├── 87 │ │ └── issue-87.spec.ts │ ├── 102 │ │ └── issue-102.spec.ts │ ├── 112 │ │ └── issue-112.spec.ts │ ├── 151 │ │ └── issue-151.spect.ts │ └── 157 │ │ └── issue-157.spec.ts └── tsconfig.json ├── tsconfig.json ├── tsconfig.prod.cjs.json ├── tsconfig.prod.esm2015.json ├── tsconfig.prod.esm5.json ├── tsconfig.prod.json ├── tsconfig.prod.types.json ├── tsconfig.spec.json └── tslint.json /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | parser: '@typescript-eslint/parser' 2 | plugins: 3 | - '@typescript-eslint' 4 | parserOptions: 5 | ecmaVersion: 2018 6 | sourceType: module 7 | project: 8 | - ./tsconfig.json 9 | - ./tsconfig.spec.json 10 | extends: 11 | - 'plugin:@typescript-eslint/recommended' 12 | - 'plugin:@typescript-eslint/recommended-requiring-type-checking' 13 | - 'plugin:jest/recommended' 14 | - 'prettier' 15 | rules: 16 | '@typescript-eslint/explicit-member-accessibility': off 17 | '@typescript-eslint/no-angle-bracket-type-assertion': off 18 | '@typescript-eslint/no-parameter-properties': off 19 | '@typescript-eslint/explicit-function-return-type': off 20 | '@typescript-eslint/member-delimiter-style': off 21 | '@typescript-eslint/no-inferrable-types': off 22 | '@typescript-eslint/no-explicit-any': off 23 | '@typescript-eslint/no-unused-vars': 24 | - 'error' 25 | - args: 'none' 26 | # TODO: Remove these and fixed issues once we merged all the current PRs. 27 | '@typescript-eslint/ban-types': off 28 | '@typescript-eslint/no-unsafe-return': off 29 | '@typescript-eslint/no-unsafe-assignment': off 30 | '@typescript-eslint/no-unsafe-call': off 31 | '@typescript-eslint/no-unsafe-member-access': off 32 | '@typescript-eslint/explicit-module-boundary-types': off -------------------------------------------------------------------------------- /.gitbook.yaml: -------------------------------------------------------------------------------- 1 | root: ./docs 2 | ​structure: 3 | readme: README.md 4 | summary: SUMMARY.md​ -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "10:00" 8 | timezone: Europe/Budapest 9 | open-pull-requests-limit: 5 10 | versioning-strategy: increase 11 | commit-message: 12 | prefix: build 13 | include: scope 14 | ignore: 15 | - dependency-name: "husky" -------------------------------------------------------------------------------- /.github/semantic.yml: -------------------------------------------------------------------------------- 1 | titleAndCommits: true 2 | allowMergeCommits: false 3 | scopes: 4 | - deps 5 | - deps-dev 6 | types: 7 | - feat 8 | - fix 9 | - docs 10 | - style 11 | - refactor 12 | - perf 13 | - test 14 | - build 15 | - ci 16 | - chore 17 | - revert 18 | - merge -------------------------------------------------------------------------------- /.github/workflows/auto-merge-dependabot-workflow.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: 3 | pull_request_target 4 | jobs: 5 | dependabot: 6 | runs-on: ubuntu-latest 7 | if: github.actor == 'dependabot[bot]' 8 | steps: 9 | - name: 'Auto approve PR by Dependabot' 10 | uses: hmarr/auto-approve-action@v2.0.0 11 | with: 12 | github-token: "${{ secrets.TYPESTACK_BOT_TOKEN }}" 13 | - name: 'Comment merge command' 14 | uses: actions/github-script@v3 15 | with: 16 | github-token: ${{secrets.TYPESTACK_BOT_TOKEN }} 17 | script: | 18 | await github.issues.createComment({ 19 | owner: context.repo.owner, 20 | repo: context.repo.repo, 21 | issue_number: context.issue.number, 22 | body: '@dependabot squash and merge' 23 | }) 24 | -------------------------------------------------------------------------------- /.github/workflows/continuous-deployment-workflow.yml: -------------------------------------------------------------------------------- 1 | 2 | name: CD 3 | on: 4 | release: 5 | types: [created] 6 | jobs: 7 | publish: 8 | name: Publish to NPM 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v2 13 | with: 14 | node-version: 'lts/*' 15 | registry-url: https://registry.npmjs.org 16 | - run: npm ci --ignore-scripts 17 | - run: npm run prettier:check 18 | - run: npm run lint:check 19 | - run: npm run test:ci 20 | - run: npm run build:es2015 21 | - run: npm run build:esm5 22 | - run: npm run build:cjs 23 | - run: npm run build:umd 24 | - run: npm run build:types 25 | - run: cp LICENSE build/LICENSE 26 | - run: cp README.md build/README.md 27 | - run: jq 'del(.devDependencies) | del(.scripts)' package.json > build/package.json 28 | - run: npm publish ./build 29 | env: 30 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/continuous-integration-workflow.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | checks: 5 | name: Linters 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - uses: actions/setup-node@v2 10 | with: 11 | node-version: 'lts/*' 12 | - run: npm ci --ignore-scripts 13 | - run: npm run prettier:check 14 | - run: npm run lint:check 15 | tests: 16 | name: Tests 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: actions/setup-node@v2 21 | with: 22 | node-version: 'lts/*' 23 | - run: npm ci --ignore-scripts 24 | - run: npm run test:ci 25 | - run: npm install codecov -g 26 | - run: codecov -f ./coverage/clover.xml -t ${{ secrets.CODECOV_TOKEN }} 27 | build: 28 | name: Build 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v2 32 | - uses: actions/setup-node@v2 33 | with: 34 | node-version: 'lts/*' 35 | - run: npm ci --ignore-scripts 36 | - run: npm run build:es2015 37 | - run: npm run build:esm5 38 | - run: npm run build:cjs 39 | - run: npm run build:umd 40 | - run: npm run build:types 41 | -------------------------------------------------------------------------------- /.github/workflows/lock-closed-issues-workflow.yml: -------------------------------------------------------------------------------- 1 | name: 'Lock inactive threads' 2 | on: 3 | schedule: 4 | - cron: '0 0 * * *' 5 | jobs: 6 | lock: 7 | name: Lock closed issues 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: dessant/lock-threads@v2 11 | with: 12 | github-token: ${{ github.token }} 13 | issue-lock-inactive-days: 30 14 | pr-lock-inactive-days: 30 15 | issue-lock-comment: > 16 | This issue has been automatically locked since there 17 | has not been any recent activity after it was closed. 18 | Please open a new issue for related bugs. 19 | pr-lock-comment: > 20 | This pull request has been automatically locked since there 21 | has not been any recent activity after it was closed. 22 | Please open a new issue for related bugs. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Log files 2 | logs 3 | *.log 4 | *.tmp 5 | *.tmp.* 6 | log.txt 7 | npm-debug.log* 8 | 9 | # Testing output 10 | lib-cov/** 11 | coverage/** 12 | 13 | # Environment files 14 | .env 15 | 16 | # Dependency directories 17 | node_modules 18 | 19 | # MacOS related files 20 | *.DS_Store 21 | .AppleDouble 22 | .LSOverride 23 | ._* 24 | UserInterfaceState.xcuserstate 25 | 26 | # Windows related files 27 | Thumbs.db 28 | Desktop.ini 29 | $RECYCLE.BIN/ 30 | 31 | # IDE - Sublime 32 | *.sublime-project 33 | *.sublime-workspace 34 | 35 | # IDE - VSCode 36 | .vscode/** 37 | !.vscode/tasks.json 38 | !.vscode/launch.json 39 | 40 | # IDE - IntelliJ 41 | .idea 42 | 43 | # Compilation output folders 44 | dist/ 45 | build/ 46 | tmp/ 47 | out-tsc/ 48 | temp 49 | 50 | # Files for playing around locally 51 | playground.ts 52 | playground.js -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | printWidth: 120 2 | tabWidth: 2 3 | useTabs: false 4 | semi: true 5 | singleQuote: true 6 | trailingComma: es5 7 | bracketSpacing: true 8 | arrowParens: avoid -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.11.0 [BREAKING] - [UNRELEASED] 4 | 5 | ### BREAKING CHANGES 6 | 7 | #### `Container.set(token, value)` and `Container.set('string-id', value)` signature removed 8 | 9 | To allow better intellisense support for signatures, the overloads allowing to specify services 10 | via a simplified form has been removed. From now on, services can be specified via the configuration object. 11 | 12 | ```ts 13 | // Old format 14 | Container.set(myToken, myValue); 15 | 16 | // New format 17 | Container.set({ id: myToken, value: myValue }); 18 | ``` 19 | 20 | #### `Service(token)` and `Service('string-id')` signature removed 21 | 22 | To allow better intellisense support for signatures, the overloads allowing to specify services 23 | via a simplified form has been removed. From now on, services can be specified via the configuration object. 24 | 25 | ```ts 26 | // Old format 27 | @Service(myToken) 28 | export class MyClass {} 29 | 30 | // New format 31 | @Service({ id: myToken }) 32 | export class MyClass {} 33 | ``` 34 | 35 | #### `Container.set({})` and `@Service()` decorator signature change 36 | 37 | The `global`, `transient` options had been removed in favor of `scope` option. 38 | 39 | ```ts 40 | // Old format 41 | @Service({ transient: true }) 42 | class MyTransientClass {} 43 | 44 | @Service({ global: true }) 45 | class MySingletonClass {} 46 | 47 | // New format 48 | @Service({ scope: 'transient' }) 49 | class MyTransientClass {} 50 | 51 | @Service({ scope: 'singleton' }) 52 | class MySingletonClass {} 53 | ``` 54 | 55 | #### `Container.reset()` signature change 56 | 57 | The `Container.reset` signature has changed. It's only possible to reset the current container instance you are calling 58 | the function on, so the first `containerId` parameter has been removed. 59 | 60 | ```ts 61 | // Old format 62 | Container.reset(myContainerId, { strategy: 'resetValue' }); 63 | 64 | // New format 65 | MyContainer.reset({ strategy: 'resetValue' }); 66 | ``` 67 | 68 | #### `Container.set([definitionOne, definitionTwo])` signature removed 69 | 70 | The option to add definitions as an array was removed. This was internally used, but exposed via the typing. 71 | 72 | 73 | ```ts 74 | // Old format 75 | Container.set([{ id: SomeClass, type: SomeClass }, { id: OtherClass, type: OtherClass }]) 76 | 77 | // New format 78 | [{ id: SomeClass, type: SomeClass }, { id: OtherClass, type: OtherClass }].map(metadata => Container.set(metadata)); 79 | ``` 80 | 81 | ### Added 82 | 83 | - added support for specifying Container ID as `Symbol` 84 | - re-enabled throwing error when `reflect-metadata` is not imported 85 | 86 | ### Changed 87 | 88 | - internally the default container is also an instance of the ContainerInstance class from now on 89 | 90 | ### Fixed 91 | 92 | - `Container.set()` correctly enforces typing when used with a `Token`. Attempting to set something else than the Token 93 | type allows will raise a compile time error 94 | 95 | ## 0.10.0 [BREAKING] - 2021.01.15 96 | 97 | ### BREAKING CHANGES 98 | 99 | #### Container.remove signature change 100 | 101 | The `Container.remove` method from now accepts one ID or an array of IDs. 102 | 103 | ```ts 104 | // Old format 105 | Container.remove(myServiceA, myServiceB); 106 | 107 | // New format 108 | Container.remove([myServiceA, myServiceB]); 109 | ``` 110 | 111 | #### Removed support for calling `Service([depA, depB], factory)` 112 | 113 | This was an undocumented way of calling the `Service` function directly instead of using it as a decorator. This option 114 | has been removed and the official supported way of achieving the same is with `Container.set`. Example: 115 | 116 | ```ts 117 | const myToken = new Token('myToken'); 118 | 119 | Container.set(myToken, 'test-value'); 120 | 121 | // Old format: 122 | const oldWayService = Service([myToken], function myFactory(myToken) { 123 | return myToken.toUpperCase(); 124 | }); 125 | const oldResult = Container.get(oldWayService); 126 | // New format 127 | const newWayService = Container.set({ 128 | // ID can be anything, we use string for simplicity 129 | id: 'my-custom-service', 130 | factory: function myFactory(container) { 131 | return container.get(myToken).toUppserCase(); 132 | }, 133 | }); 134 | const newResult = Container.get('my-custom-service'); 135 | 136 | oldResult === newResult; // -> true, both equals to "TEST-VALUE" 137 | ``` 138 | 139 | ### Added 140 | 141 | - added `eager` option to `ServiceOptions`, when enabled the class will be instantiated as soon as it's registered in the container 142 | - added support for destroying removed services, when a service is removed and has a callable `destroy` property it will be called by TypeDI 143 | 144 | ### Changed 145 | 146 | - [BREAKING] removed old, undocumented way of calling `@Service` decorator directly 147 | - [BREAKING] renamed `MissingProvidedServiceTypeError` to `CannotInstantiateValueError` 148 | - various internal refactors 149 | - updated various dev dependencies 150 | 151 | ### Fixed 152 | 153 | - generated sourcemaps contains the Typescript files preventing reference errors when using TypeDI with various build tools 154 | 155 | ## 0.9.1 - 2021.01.11 156 | 157 | ### Fixed 158 | 159 | - correctly export error classes from package root 160 | 161 | ## 0.9.0 - 2021.01.10 162 | 163 | ### BREAKING CHANGES 164 | 165 | #### Unregistered types are not resolved 166 | 167 | Prior to this version when an unknown constructable type was requested from the default container it was added automatically 168 | to the container and returned. This behavior has changed and now a `ServiceNotFoundError` error is thrown. 169 | 170 | #### Changed container reset behavior 171 | 172 | Until now resetting a container removed all dependency declarations from the container. From now on the default behavior 173 | is to remove the created instances only but not the definitions. This means requesting a Service again from the container 174 | won't result in a `ServiceNotFoundError` but will create a new instance of the requested function again. 175 | 176 | The old behavior can be restored with passing the `{ strategy: 'resetServices'}` to the `ContainerInstance.reset` function. 177 | 178 | ### Changed 179 | 180 | - **[BREAKING]** unknown values are not resolved anymore (ref #87) 181 | - **[BREAKING]** resetting a container doesn't remove the service definitions only the created instances by default 182 | - **[BREAKING]** container ID can be string only now 183 | - default container ID changed from `undefined` to `default` 184 | - stricter type definitions and assertions across the project 185 | - updated the wording of `ServiceNotFoundError` to better explain which service is missing (#138) 186 | - updated various dev-dependencies 187 | - various changes to project tooling 188 | 189 | ### Fixed 190 | 191 | - fixed a bug where requesting service with circular dependencies from a scoped container would result in Maximum call stack size exceeded error (ref #112) 192 | - fixed a bug where `@Inject`-ed properties were not injected in inherited child classes (ref #102) 193 | - fixed a typing issue which prevented using abstract class as service identifier (ref #144) 194 | - fixed a bug which broke transient services when `Container.reset()` was called (ref #157) 195 | - fixed some typos in the getting started documentation 196 | 197 | ## 0.8.0 198 | 199 | - added new type of dependency injection - function DI 200 | - now null can be stored in the container for values 201 | 202 | ## 0.7.2 203 | 204 | - fixed bug with inherited services 205 | 206 | ## 0.7.1 207 | 208 | - fixed the way how global services work 209 | 210 | ## 0.7.0 211 | 212 | - added javascript support 213 | - removed deprecated `@Require` decorator 214 | - added support for transient services 215 | - now service constructors cannot accept non-service arguments 216 | - added `@InjectMany` decorator to support injection of "many" values 217 | - fixed the way how global services work 218 | 219 | ## 0.6.1 220 | 221 | - added `Container.has` method 222 | 223 | ## 0.6.0 224 | 225 | - added multiple containers support 226 | - added grouped (tagged) containers support 227 | - removed `provide` method, use `set` method instead 228 | - deprecated `Require` decorator. Use es6 imports instead or named services 229 | - inherited classes don't need to be decorated with `@Service` decorator 230 | - other small api changes 231 | - now `Handler`'s `value` accepts a container which requests the value 232 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2015-2021 TypeStack 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TypeDI 2 | 3 | ![Build Status](https://github.com/typestack/typedi/workflows/CI/badge.svg) 4 | [![codecov](https://codecov.io/gh/typestack/typedi/branch/master/graph/badge.svg)](https://codecov.io/gh/typestack/typedi) 5 | [![npm version](https://badge.fury.io/js/typedi.svg)](https://badge.fury.io/js/typedi) 6 | [![Dependency Status](https://david-dm.org/typestack/typedi.svg)](https://david-dm.org/typestack/typedi) 7 | 8 | TypeDI is a [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) tool for TypeScript and JavaScript. With it you can build well-structured and easily testable applications in Node or in the browser. 9 | 10 | Main features includes: 11 | 12 | - property based injection 13 | - constructor based injection 14 | - singleton and transient services 15 | - support for multiple DI containers 16 | 17 | ## Installation 18 | 19 | > Note: This installation guide is for usage with TypeScript, if you wish to use 20 | > TypeDI without Typescript please read the documentation about how get started. 21 | 22 | To start using TypeDI install the required packages via NPM: 23 | 24 | ```bash 25 | npm install typedi reflect-metadata 26 | ``` 27 | 28 | Import the `reflect-metadata` package at the **first line** of your application: 29 | 30 | ```ts 31 | import 'reflect-metadata'; 32 | 33 | // Your other imports and initialization code 34 | // comes here after you imported the reflect-metadata package! 35 | ``` 36 | 37 | As a last step, you need to enable emitting decorator metadata in your Typescript config. Add these two lines to your `tsconfig.json` file under the `compilerOptions` key: 38 | 39 | ```json 40 | "emitDecoratorMetadata": true, 41 | "experimentalDecorators": true, 42 | ``` 43 | 44 | Now you are ready to use TypeDI with Typescript! 45 | 46 | ## Basic Usage 47 | 48 | ```ts 49 | import { Container, Service } from 'typedi'; 50 | 51 | @Service() 52 | class ExampleInjectedService { 53 | printMessage() { 54 | console.log('I am alive!'); 55 | } 56 | } 57 | 58 | @Service() 59 | class ExampleService { 60 | constructor( 61 | // because we annotated ExampleInjectedService with the @Service() 62 | // decorator TypeDI will automatically inject an instance of 63 | // ExampleInjectedService here when the ExampleService class is requested 64 | // from TypeDI. 65 | public injectedService: ExampleInjectedService 66 | ) {} 67 | } 68 | 69 | const serviceInstance = Container.get(ExampleService); 70 | // we request an instance of ExampleService from TypeDI 71 | 72 | serviceInstance.injectedService.printMessage(); 73 | // logs "I am alive!" to the console 74 | ``` 75 | 76 | ## Documentation 77 | 78 | The detailed usage guide and API documentation for the project can be found: 79 | 80 | - at [docs.typestack.community/typedi][docs-stable] 81 | - in the `./docs` folder of the repository 82 | 83 | [docs-stable]: https://docs.typestack.community/typedi/ 84 | [docs-development]: https://docs.typestack.community/typedi/v/develop/ 85 | 86 | ## Contributing 87 | 88 | Please read our [contributing guidelines](./CONTRIBUTING.md) to get started. 89 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: 70..100 3 | round: down 4 | precision: 2 5 | status: 6 | project: 7 | default: 8 | threshold: 0% 9 | paths: 10 | - src/**/*.ts 11 | comment: off 12 | ignore: 13 | - testing/**/*.ts 14 | - src/**/*.interface.ts -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | ## Typescript Usage 4 | 5 | With TypeDI you can use a named services. Example: 6 | 7 | ```typescript 8 | import { Container, Service, Inject } from 'typedi'; 9 | 10 | interface Factory { 11 | create(): void; 12 | } 13 | 14 | @Service({ id: 'bean.factory' }) 15 | class BeanFactory implements Factory { 16 | create() {} 17 | } 18 | 19 | @Service({ id: 'sugar.factory' }) 20 | class SugarFactory implements Factory { 21 | create() {} 22 | } 23 | 24 | @Service({ id: 'water.factory' }) 25 | class WaterFactory implements Factory { 26 | create() {} 27 | } 28 | 29 | @Service({ id: 'coffee.maker' }) 30 | class CoffeeMaker { 31 | beanFactory: Factory; 32 | sugarFactory: Factory; 33 | 34 | @Inject('water.factory') 35 | waterFactory: Factory; 36 | 37 | constructor(@Inject('bean.factory') beanFactory: BeanFactory, @Inject('sugar.factory') sugarFactory: SugarFactory) { 38 | this.beanFactory = beanFactory; 39 | this.sugarFactory = sugarFactory; 40 | } 41 | 42 | make() { 43 | this.beanFactory.create(); 44 | this.sugarFactory.create(); 45 | this.waterFactory.create(); 46 | } 47 | } 48 | 49 | let coffeeMaker = Container.get('coffee.maker'); 50 | coffeeMaker.make(); 51 | ``` 52 | 53 | This feature especially useful if you want to store (and inject later on) some settings or configuration options. 54 | For example: 55 | 56 | ```typescript 57 | import { Container, Service, Inject } from 'typedi'; 58 | 59 | // somewhere in your global app parameters 60 | Container.set('authorization-token', 'RVT9rVjSVN'); 61 | 62 | @Service() 63 | class UserRepository { 64 | @Inject('authorization-token') 65 | authorizationToken: string; 66 | } 67 | ``` 68 | 69 | When you write tests you can easily provide your own "fake" dependencies to classes you are testing using `set` method: 70 | `provide` methods of the container: 71 | 72 | ```typescript 73 | Container.set(CoffeeMaker, new FakeCoffeeMaker()); 74 | 75 | // or for named services 76 | 77 | Container.set([ 78 | { id: 'bean.factory', value: new FakeBeanFactory() }, 79 | { id: 'sugar.factory', value: new FakeSugarFactory() }, 80 | { id: 'water.factory', value: new FakeWaterFactory() }, 81 | ]); 82 | ``` 83 | 84 | ## TypeScript Advanced Usage Examples 85 | 86 | - [Using factory function to create service](#using-factory-function-to-create-service) 87 | - [Using factory class to create service](#using-factory-class-to-create-service) 88 | - [Problem with circular references](#problem-with-circular-references) 89 | - [Custom decorators](#custom-decorators) 90 | - [Using service groups](#using-service-groups) 91 | - [Using multiple containers and scoped containers](#using-multiple-containers-and-scoped-containers) 92 | - [Remove registered services or reset container state](#remove-registered-services-or-reset-container-state) 93 | 94 | ### Using factory function to create service 95 | 96 | You can create your services with the container using factory functions. 97 | 98 | This way, service instance will be created by calling your factory function instead of 99 | instantiating a class directly. 100 | 101 | ```typescript 102 | import { Container, Service } from 'typedi'; 103 | 104 | function createCar() { 105 | return new Car('V8'); 106 | } 107 | 108 | @Service({ factory: createCar }) 109 | class Car { 110 | constructor(public engineType: string) {} 111 | } 112 | 113 | // Getting service from the container. 114 | // Service will be created by calling the specified factory function. 115 | const car = Container.get(Car); 116 | 117 | console.log(car.engineType); // > "V8" 118 | ``` 119 | 120 | ### Using factory class to create service 121 | 122 | You can also create your services using factory classes. 123 | 124 | This way, service instance will be created by calling given factory service's method factory instead of 125 | instantiating a class directly. 126 | 127 | ```typescript 128 | import { Container, Service } from 'typedi'; 129 | 130 | @Service() 131 | class CarFactory { 132 | constructor(public logger: LoggerService) {} 133 | 134 | create() { 135 | return new Car('BMW', this.logger); 136 | } 137 | } 138 | 139 | @Service({ factory: [CarFactory, 'create'] }) 140 | class Car { 141 | constructor(public model: string, public logger: LoggerInterface) {} 142 | } 143 | ``` 144 | 145 | ### Problem with circular references 146 | 147 | There is a known issue in language that it can't handle circular references. For example: 148 | 149 | ```typescript 150 | // Car.ts 151 | @Service() 152 | export class Car { 153 | @Inject() 154 | engine: Engine; 155 | } 156 | 157 | // Engine.ts 158 | @Service() 159 | export class Engine { 160 | @Inject() 161 | car: Car; 162 | } 163 | ``` 164 | 165 | This code will not work, because Engine has a reference to Car, and Car has a reference to Engine. 166 | One of them will be undefined and it cause errors. To fix them you need to specify a type in a function this way: 167 | 168 | ```typescript 169 | // Car.ts 170 | @Service() 171 | export class Car { 172 | @Inject(type => Engine) 173 | engine: Engine; 174 | } 175 | 176 | // Engine.ts 177 | @Service() 178 | export class Engine { 179 | @Inject(type => Car) 180 | car: Car; 181 | } 182 | ``` 183 | 184 | And that's all. This does **NOT** work for constructor injections. 185 | 186 | ### Custom decorators 187 | 188 | You can create your own decorators which will inject your given values for your service dependencies. 189 | For example: 190 | 191 | ```typescript 192 | // Logger.ts 193 | export function Logger() { 194 | return function (object: Object, propertyName: string, index?: number) { 195 | const logger = new ConsoleLogger(); 196 | Container.registerHandler({ object, propertyName, index, value: containerInstance => logger }); 197 | }; 198 | } 199 | 200 | // LoggerInterface.ts 201 | export interface LoggerInterface { 202 | log(message: string): void; 203 | } 204 | 205 | // ConsoleLogger.ts 206 | import { LoggerInterface } from './LoggerInterface'; 207 | 208 | export class ConsoleLogger implements LoggerInterface { 209 | log(message: string) { 210 | console.log(message); 211 | } 212 | } 213 | 214 | // UserRepository.ts 215 | @Service() 216 | export class UserRepository { 217 | constructor(@Logger() private logger: LoggerInterface) {} 218 | 219 | save(user: User) { 220 | this.logger.log(`user ${user.firstName} ${user.secondName} has been saved.`); 221 | } 222 | } 223 | ``` 224 | 225 | ### Using service groups 226 | 227 | You can group multiple services into single group tagged with service id or token. 228 | For example: 229 | 230 | ```typescript 231 | // Factory.ts 232 | export interface Factory { 233 | create(): any; 234 | } 235 | 236 | // FactoryToken.ts 237 | export const FactoryToken = new Token('factories'); 238 | 239 | // BeanFactory.ts 240 | @Service({ id: FactoryToken, multiple: true }) 241 | export class BeanFactory implements Factory { 242 | create() { 243 | console.log('bean created'); 244 | } 245 | } 246 | 247 | // SugarFactory.ts 248 | @Service({ id: FactoryToken, multiple: true }) 249 | export class SugarFactory implements Factory { 250 | create() { 251 | console.log('sugar created'); 252 | } 253 | } 254 | 255 | // WaterFactory.ts 256 | @Service({ id: FactoryToken, multiple: true }) 257 | export class WaterFactory implements Factory { 258 | create() { 259 | console.log('water created'); 260 | } 261 | } 262 | 263 | // app.ts 264 | // now you can get all factories in a single array 265 | Container.import([BeanFactory, SugarFactory, WaterFactory]); 266 | const factories = Container.getMany(FactoryToken); // factories is Factory[] 267 | factories.forEach(factory => factory.create()); 268 | ``` 269 | 270 | ### Using multiple containers and scoped containers 271 | 272 | By default all services are stored in the global service container, 273 | and this global service container holds all unique instances of each service you have. 274 | 275 | If you want your services to behave and store data inside differently, 276 | based on some user context (http request for example) - 277 | you can use different containers for different contexts. 278 | For example: 279 | 280 | ```typescript 281 | // QuestionController.ts 282 | @Service() 283 | export class QuestionController { 284 | constructor(protected questionRepository: QuestionRepository) {} 285 | 286 | save() { 287 | this.questionRepository.save(); 288 | } 289 | } 290 | 291 | // QuestionRepository.ts 292 | @Service() 293 | export class QuestionRepository { 294 | save() {} 295 | } 296 | 297 | // app.ts 298 | const request1 = { param: 'question1' }; 299 | const controller1 = Container.of(request1).get(QuestionController); 300 | controller1.save('Timber'); 301 | Container.reset(request1); 302 | 303 | const request2 = { param: 'question2' }; 304 | const controller2 = Container.of(request2).get(QuestionController); 305 | controller2.save(''); 306 | Container.reset(request2); 307 | ``` 308 | 309 | In this example `controller1` and `controller2` are completely different instances, 310 | and `QuestionRepository` used in those controllers are different instances as well. 311 | 312 | `Container.reset` removes container with the given context identifier. 313 | If you want your services to be completely global and not be container-specific, 314 | you can mark them as global: 315 | 316 | ```typescript 317 | @Service({ global: true }) 318 | export class QuestionUtils {} 319 | ``` 320 | 321 | And this global service will be the same instance across all containers. 322 | 323 | TypeDI also supports a function dependency injection. Here is how it looks like: 324 | 325 | ```javascript 326 | export const PostRepository = Service(() => ({ 327 | getName() { 328 | return 'hello from post repository'; 329 | }, 330 | })); 331 | 332 | export const PostManager = Service(() => ({ 333 | getId() { 334 | return 'some post id'; 335 | }, 336 | })); 337 | 338 | export class PostQueryBuilder { 339 | build() { 340 | return 'SUPER * QUERY'; 341 | } 342 | } 343 | 344 | export const PostController = Service( 345 | [PostManager, PostRepository, PostQueryBuilder], 346 | (manager, repository, queryBuilder) => { 347 | return { 348 | id: manager.getId(), 349 | name: repository.getName(), 350 | query: queryBuilder.build(), 351 | }; 352 | } 353 | ); 354 | 355 | const postController = Container.get(PostController); 356 | console.log(postController); 357 | ``` 358 | 359 | ### Remove registered services or reset container state 360 | 361 | If you need to remove registered service from container simply use `Container.remove(...)` method. 362 | Also you can completely reset the container by calling `Container.reset()` method. 363 | This will effectively remove all registered services from the container. 364 | -------------------------------------------------------------------------------- /docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Table of contents 2 | 3 | - [Old documentation](README.md) 4 | 5 | - [Getting Started](typescript/01-getting-started.md) 6 | - [Usage Guide](typescript/02-basic-usage-guide.md) 7 | - [Container API](typescript/03-container-api.md) 8 | - [@Service decorator](typescript/04-service-decorator.md) 9 | - [@Inject decorator](typescript/05-inject-decorator.md) 10 | - [Service Tokens](typescript/06-service-tokens.md) 11 | - [Inheritance](typescript/07-inheritance.md) 12 | - [Usage with TypeORM](typescript/07-usage-with-typeorm.md) 13 | - Advanced Usage 14 | - [Creating custom decorators](typescript/08-custom-decorators.md) 15 | - [Using scoped container](typescript/09-using-scoped-containers.md) 16 | - [Transient services](typescript/10-using-transient-services.md) 17 | 18 | ## Usage without TypeScript 19 | 20 | - [Getting Started](javascript/01-getting-started.md) 21 | - Usage 22 | - [Old documentation](javascript/02-basic-usage.md) 23 | -------------------------------------------------------------------------------- /docs/javascript/01-getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting started without TypeScript 2 | 3 | It's possible to use TypeDI without TypesScript, however some of the functionality is limited or not available. 4 | These differences are listed below in the [Limitations][limitations-sections] section. 5 | 6 | ## Installation 7 | 8 | To start using TypeDI with JavaScript install the required packages via NPM: 9 | 10 | ```bash 11 | npm install typedi reflect-metadata 12 | ``` 13 | 14 | ## Basic usage 15 | 16 | The most basic usage is to request an instance of a class definition. TypeDI will check if an instance of the class has 17 | been created before and return the cached version or it will create a new instance, cache and return it. 18 | 19 | ```js 20 | import 'reflect-metadata'; 21 | import { Container } from 'typedi'; 22 | 23 | class ExampleClass { 24 | print() { 25 | console.log('I am alive!'); 26 | } 27 | } 28 | 29 | /** Register this class to the TypeDI container */ 30 | Container.set({ id: ExampleClass, type: ExampleClass }); 31 | 32 | /** Request an instance of ExampleClass from TypeDI. */ 33 | const classInstance = Container.get(ExampleClass); 34 | 35 | /** We received an instance of ExampleClass and ready to work with it. */ 36 | classInstance.print(); 37 | ``` 38 | 39 | For more advanced usage examples and patterns please read the [next page][basic-usage-page]. 40 | 41 | ## Limitations 42 | 43 | When registering your dependencies with the `Container.set()` method, there are three options available that must be set. Either one of the following are allowed: `type`, `factory`, or `value` but not more than one. 44 | 45 | - `Container.set({ id: ExampleClass, type: ExampleClass});` 46 | - `Container.set({ id: ExampleClass, value: new ExampleClass});` 47 | - `Container.set({ id: ExampleClass, factory: ExampleClass});` 48 | 49 | To get started quickly, it is recommend to use `type` due to the fact that using `value` will instantiate the class before it's registered to the TypeDI Container. Using `type` will also assure that the TypeDI Container is injected to the constructor. 50 | 51 | [limitations-sections]: #limitations 52 | [basic-usage-page]: ./02-basic-usage.md 53 | -------------------------------------------------------------------------------- /docs/javascript/02-basic-usage.md: -------------------------------------------------------------------------------- 1 | # Usage without TypeScript 2 | 3 | > **NOTE:** This page is a direct copy of the old documentation. It will be reworked. 4 | 5 | In your class's constructor you always receive as a last argument a container which you can use to get other dependencies. 6 | 7 | ```javascript 8 | class BeanFactory { 9 | create() {} 10 | } 11 | 12 | class SugarFactory { 13 | create() {} 14 | } 15 | 16 | class WaterFactory { 17 | create() {} 18 | } 19 | 20 | class CoffeeMaker { 21 | constructor(container) { 22 | this.beanFactory = container.get(BeanFactory); 23 | this.sugarFactory = container.get(SugarFactory); 24 | this.waterFactory = container.get(WaterFactory); 25 | } 26 | 27 | make() { 28 | this.beanFactory.create(); 29 | this.sugarFactory.create(); 30 | this.waterFactory.create(); 31 | } 32 | } 33 | 34 | var Container = require('typedi').Container; 35 | var coffeeMaker = Container.get(CoffeeMaker); 36 | coffeeMaker.make(); 37 | ``` 38 | 39 | With TypeDI you can use a named services. Example: 40 | 41 | ```javascript 42 | var Container = require('typedi').Container; 43 | 44 | class BeanFactory implements Factory { 45 | create() {} 46 | } 47 | 48 | class SugarFactory implements Factory { 49 | create() {} 50 | } 51 | 52 | class WaterFactory implements Factory { 53 | create() {} 54 | } 55 | 56 | class CoffeeMaker { 57 | beanFactory: Factory; 58 | sugarFactory: Factory; 59 | waterFactory: Factory; 60 | 61 | constructor(container) { 62 | this.beanFactory = container.get('bean.factory'); 63 | this.sugarFactory = container.get('sugar.factory'); 64 | this.waterFactory = container.get('water.factory'); 65 | } 66 | 67 | make() { 68 | this.beanFactory.create(); 69 | this.sugarFactory.create(); 70 | this.waterFactory.create(); 71 | } 72 | } 73 | 74 | Container.set('bean.factory', new BeanFactory(Container)); 75 | Container.set('sugar.factory', new SugarFactory(Container)); 76 | Container.set('water.factory', new WaterFactory(Container)); 77 | Container.set('coffee.maker', new CoffeeMaker(Container)); 78 | 79 | var coffeeMaker = Container.get('coffee.maker'); 80 | coffeeMaker.make(); 81 | ``` 82 | 83 | This feature especially useful if you want to store (and inject later on) some settings or configuration options. 84 | For example: 85 | 86 | ```javascript 87 | var Container = require('typedi').Container; 88 | 89 | // somewhere in your global app parameters 90 | Container.set('authorization-token', 'RVT9rVjSVN'); 91 | 92 | class UserRepository { 93 | constructor(container) { 94 | this.authorizationToken = container.get('authorization-token'); 95 | } 96 | } 97 | ``` 98 | 99 | When you write tests you can easily provide your own "fake" dependencies to classes you are testing using `set` method: 100 | 101 | ```javascript 102 | Container.set(CoffeeMaker, new FakeCoffeeMaker()); 103 | 104 | // or for named services 105 | 106 | Container.set([ 107 | { id: 'bean.factory', value: new FakeBeanFactory() }, 108 | { id: 'sugar.factory', value: new FakeSugarFactory() }, 109 | { id: 'water.factory', value: new FakeWaterFactory() }, 110 | ]); 111 | ``` 112 | 113 | TypeDI also supports a function dependency injection. Here is how it looks like: 114 | 115 | ```javascript 116 | var Service = require('typedi').Service; 117 | var Container = require('typedi').Container; 118 | 119 | var PostRepository = Service(() => ({ 120 | getName() { 121 | return 'hello from post repository'; 122 | }, 123 | })); 124 | 125 | var PostManager = Service(() => ({ 126 | getId() { 127 | return 'some post id'; 128 | }, 129 | })); 130 | 131 | class PostQueryBuilder { 132 | build() { 133 | return 'SUPER * QUERY'; 134 | } 135 | } 136 | 137 | var PostController = Service([PostManager, PostRepository, PostQueryBuilder], (manager, repository, queryBuilder) => { 138 | return { 139 | id: manager.getId(), 140 | name: repository.getName(), 141 | query: queryBuilder.build(), 142 | }; 143 | }); 144 | 145 | var postController = Container.get(PostController); 146 | console.log(postController); 147 | ``` 148 | -------------------------------------------------------------------------------- /docs/typescript/01-getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | TypeDI is a [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) library for TypeScript and JavaScript. 4 | 5 | ## Installation 6 | 7 | > Note: This installation guide is for usage with TypeScript, if you wish to use TypeDI without Typescript 8 | > please read the [getting started guide][getting-started-js] for JavaScript. 9 | 10 | To start using TypeDI install the required packages via NPM: 11 | 12 | ```bash 13 | npm install typedi reflect-metadata 14 | ``` 15 | 16 | Import the `reflect-metadata` package at the **first line** of your application: 17 | 18 | ```ts 19 | import 'reflect-metadata'; 20 | 21 | // Your other imports and initialization code 22 | // comes here after you imported the reflect-metadata package! 23 | ``` 24 | 25 | As the last step, you need to enable emitting decorator metadata in your Typescript config. Add these two lines to your `tsconfig.json` file under the `compilerOptions` key: 26 | 27 | ```json 28 | "emitDecoratorMetadata": true, 29 | "experimentalDecorators": true, 30 | ``` 31 | 32 | Now you are ready to use TypeDI with Typescript! 33 | 34 | ## Basic Usage 35 | 36 | The most basic usage is to request an instance of a class definition. TypeDI will check if an instance of the class has 37 | been created before and return the cached version or it will create a new instance, cache, and return it. 38 | 39 | ```ts 40 | import { Container, Service } from 'typedi'; 41 | 42 | @Service() 43 | class ExampleInjectedService { 44 | printMessage() { 45 | console.log('I am alive!'); 46 | } 47 | } 48 | 49 | @Service() 50 | class ExampleService { 51 | constructor( 52 | // because we annotated ExampleInjectedService with the @Service() 53 | // decorator TypeDI will automatically inject an instance of 54 | // ExampleInjectedService here when the ExampleService class is requested 55 | // from TypeDI. 56 | public injectedService: ExampleInjectedService 57 | ) {} 58 | } 59 | 60 | const serviceInstance = Container.get(ExampleService); 61 | // we request an instance of ExampleService from TypeDI 62 | 63 | serviceInstance.injectedService.printMessage(); 64 | // logs "I am alive!" to the console 65 | ``` 66 | 67 | [getting-started-js]: ../javascript/01-getting-started.md 68 | -------------------------------------------------------------------------------- /docs/typescript/02-basic-usage-guide.md: -------------------------------------------------------------------------------- 1 | # Basic Usage 2 | 3 | > **IMPORTANT NOTE:** 4 | > Don't forget to **annotate your classes with the `@Service` decorator**! Both the ones being injected and those which 5 | > requests the dependencies should be annotated. 6 | 7 | ## Registering dependencies 8 | 9 | There are three ways to register your dependencies: 10 | 11 | - annotating a class with the `@Service()` decorator ([documentation](./04-service-decorator.md)) 12 | - registering a value with a `Token` 13 | - registering a value with a string identifier 14 | 15 | The `Token` and string identifier can be used to register other values than classes. Both tokens and string identifiers 16 | can register any type of value including primitive values except `undefined`. They must be set on the container with the 17 | `Container.set()` function before they can be requested via `Container.get()`. 18 | 19 | ```ts 20 | import 'reflect-metadata'; 21 | import { Container, Inject, Service, Token } from 'typedi'; 22 | 23 | const myToken = new Token('SECRET_VALUE_KEY'); 24 | 25 | Container.set(myToken, 'my-secret-value'); 26 | Container.set('my-config-key', 'value-for-config-key'); 27 | Container.set('default-pagination', 30); 28 | 29 | // somewhere else in your application 30 | const tokenValue = Container.get(myToken); 31 | const configValue = Container.get('my-config-key'); 32 | const defaultPagination = Container.get('default-pagination'); 33 | ``` 34 | 35 | _For detailed documentation about `@Service` decorator please read the [@Service decorator](./04-service-decorator.md) page._ 36 | 37 | ## Injecting dependencies 38 | 39 | There are three ways to inject your dependencies: 40 | 41 | - automatic class constructor parameter injection 42 | - annotating class properties with the `@Inject()` decorator 43 | - directly using `Container.get()` to request an instance of a class, `Token` or string identifier 44 | 45 | ### Constructor argument injection 46 | 47 | Any class which has been marked with the `@Service()` decorator will have its constructor properties automatically 48 | injected with the correct dependency. 49 | 50 | **TypeDI inserts the container instance** which was used to resolve the dependencies **as the last parameter in the constructor**. 51 | 52 | ```ts 53 | import 'reflect-metadata'; 54 | import { Container, Inject, Service } from 'typedi'; 55 | 56 | @Service() 57 | class InjectedClass {} 58 | 59 | @Service() 60 | class ExampleClass { 61 | constructor(public injectedClass: InjectedClass) {} 62 | } 63 | 64 | const instance = Container.get(ExampleClass); 65 | 66 | console.log(instance.injectedClass instanceof InjectedClass); 67 | // prints true as TypeDI assigned the instance of InjectedClass to the property 68 | ``` 69 | 70 | ### Property injection 71 | 72 | Any property which has been marked with the `@Inject` decorator will be automatically assigned the instance of the class 73 | when the parent class is initialized by TypeDI. 74 | 75 | ```ts 76 | import 'reflect-metadata'; 77 | import { Container, Inject, Service } from 'typedi'; 78 | 79 | @Service() 80 | class InjectedClass {} 81 | 82 | @Service() 83 | class ExampleClass { 84 | @Inject() 85 | injectedClass: InjectedClass; 86 | } 87 | 88 | const instance = Container.get(ExampleClass); 89 | 90 | console.log(instance.injectedClass instanceof InjectedClass); 91 | // prints true as the instance of InjectedClass has been assigned to the `injectedClass` property by TypeDI 92 | ``` 93 | 94 | _For detailed documentation about `@Inject` decorator please read the [@Inject decorator](./05-inject-decorator.md) page._ 95 | 96 | ### Using `Container.get()` 97 | 98 | The `Container.get()` function can be used directly to request an instance of the target type. TypeDI will resolve and 99 | initialize all dependency on the target class. `Container.get()` can be used to request: 100 | 101 | - a constructable value (class definition) which will return the class instance 102 | - a `Token` which will return the value registered for that `Token` 103 | - a string which will return the value registered with that name 104 | 105 | ```ts 106 | import 'reflect-metadata'; 107 | import { Container, Inject, Service, Token } from 'typedi'; 108 | 109 | const myToken = new Token('SECRET_VALUE_KEY'); 110 | 111 | @Service() 112 | class InjectedClass {} 113 | 114 | @Service() 115 | class ExampleClass { 116 | @Inject() 117 | injectedClass: InjectedClass; 118 | } 119 | 120 | /** Tokens must be explicity set in the Container with the desired value. */ 121 | Container.set(myToken, 'my-secret-value'); 122 | /** String identifier must be explicity set in the Container with the desired value. */ 123 | Container.set('my-dependency-name-A', InjectedClass); 124 | Container.set('my-dependency-name-B', 'primitive-value'); 125 | 126 | const injectedClassInstance = Container.get(InjectedClass); 127 | // a class without dependencies can be required 128 | const exampleClassInstance = Container.get(ExampleClass); 129 | // a class with dependencies can be required and dependencies will be resolved 130 | const tokenValue = Container.get(myToken); 131 | // tokenValue will be 'my-secret-value' 132 | const stringIdentifierValueA = Container.get('my-dependency-name-A'); 133 | // stringIdentifierValueA will be instance of InjectedClass 134 | const stringIdentifierValueB = Container.get('my-dependency-name-B'); 135 | // stringIdentifierValueB will be 'primitive-value' 136 | ``` 137 | 138 | _For detailed documentation about `Token` class please read the [Service Tokens](./06-service-tokens.md) page._ 139 | 140 | ## Singleton vs transient classes 141 | 142 | Every registered service by default is a singleton. Meaning repeated calls to `Container.get(MyClass)` will return the 143 | same instance. If this is not the desired behavior a class can be marked as `transient` via the `@Service()` decorator. 144 | 145 | ```ts 146 | import 'reflect-metadata'; 147 | import { Container, Inject, Service } from 'typedi'; 148 | 149 | @Service({ scope: 'transient' }) 150 | class ExampleTransientClass { 151 | constructor() { 152 | console.log('I am being created!'); 153 | // this line will be printed twice 154 | } 155 | } 156 | 157 | const instanceA = Container.get(ExampleTransientClass); 158 | const instanceB = Container.get(ExampleTransientClass); 159 | 160 | console.log(instanceA !== instanceB); 161 | // prints true 162 | ``` 163 | -------------------------------------------------------------------------------- /docs/typescript/03-container-api.md: -------------------------------------------------------------------------------- 1 | # Container API 2 | -------------------------------------------------------------------------------- /docs/typescript/04-service-decorator.md: -------------------------------------------------------------------------------- 1 | # `@Service` decorator 2 | -------------------------------------------------------------------------------- /docs/typescript/05-inject-decorator.md: -------------------------------------------------------------------------------- 1 | # The `@Inject` decorator 2 | 3 | The `@Inject()` decorator is a **property and parameter decorator** used to resolve dependencies on a class property or 4 | a constructor parameter. By default TypeDI infers the type of the property or argument and initializes an instance of 5 | the detected type, however, this behavior can be overwritten via specifying a custom constructable type, `Token`, or 6 | named service as the first parameter of the `@Inject()` decorator. 7 | 8 | ## Property injection 9 | 10 | This decorator is **mandatory** on properties where a class instance is desired. (Without the decorator, the property 11 | will stay undefined.) The type of the property is automatically inferred when it is a class, in all other cases the 12 | requested type must be provided. 13 | 14 | ```ts 15 | import 'reflect-metadata'; 16 | import { Container, Inject, Service } from 'typedi'; 17 | 18 | @Service() 19 | class InjectedExampleClass { 20 | print() { 21 | console.log('I am alive!'); 22 | } 23 | } 24 | 25 | @Service() 26 | class ExampleClass { 27 | @Inject() 28 | withDecorator: InjectedExampleClass; 29 | 30 | withoutDecorator: InjectedExampleClass; 31 | } 32 | 33 | const instance = Container.get(ExampleClass); 34 | 35 | /** 36 | * The `instance` variable is an ExampleClass instance with the `withDecorator` 37 | * property containing an InjectedExampleClass instance and `withoutDecorator` 38 | * property being undefined. 39 | */ 40 | console.log(instance); 41 | 42 | instance.withDecorator.print(); 43 | // prints "I am alive!" (InjectedExampleClass.print function) 44 | console.log(instance.withoutDecorator); 45 | // logs undefined, as this property was not marked with an @Inject decorator 46 | ``` 47 | 48 | ## Constructor Injection 49 | 50 | The `@Inject` decorator is not required in constructor injection when a class is marked with the `@Service` decorator. 51 | TypeDI will automatically infer and inject the correct class instances for every constructor argument. However, it can 52 | be used to overwrite the injected type. 53 | 54 | ```ts 55 | import 'reflect-metadata'; 56 | import { Container, Inject, Service } from 'typedi'; 57 | 58 | @Service() 59 | class InjectedExampleClass { 60 | print() { 61 | console.log('I am alive!'); 62 | } 63 | } 64 | 65 | @Service() 66 | class ExampleClass { 67 | constructor( 68 | @Inject() 69 | public withDecorator: InjectedExampleClass, 70 | public withoutDecorator: InjectedExampleClass 71 | ) {} 72 | } 73 | 74 | const instance = Container.get(ExampleClass); 75 | 76 | /** 77 | * The `instance` variable is an ExampleClass instance with both the 78 | * `withDecorator` and `withoutDecorator` property containing an 79 | * InjectedExampleClass instance. 80 | */ 81 | console.log(instance); 82 | 83 | instance.withDecorator.print(); 84 | // prints "I am alive!" (InjectedExampleClass.print function) 85 | instance.withoutDecorator.print(); 86 | // prints "I am alive!" (InjectedExampleClass.print function) 87 | ``` 88 | 89 | ## Explicitly requesting target type 90 | 91 | By default, TypeDI will try to infer the type of property and arguments and inject the proper class instance. When this 92 | is not possible (eg: the property type is an interface) there is three way to overwrite the type of the injected value: 93 | 94 | - via `@Inject(() => type)` where `type` is a constructable value (eg: a class definition) 95 | - via `@Inject(myToken)` where `myToken` is an instance of `Token` class 96 | - via `@Inject(serviceName)` where `serviceName` is a string ID 97 | 98 | In all three cases the requested dependency must be registered in the container first. 99 | 100 | ```ts 101 | import 'reflect-metadata'; 102 | import { Container, Inject, Service } from 'typedi'; 103 | 104 | @Service() 105 | class InjectedExampleClass { 106 | print() { 107 | console.log('I am alive!'); 108 | } 109 | } 110 | 111 | @Service() 112 | class BetterInjectedClass { 113 | print() { 114 | console.log('I am a different class!'); 115 | } 116 | } 117 | 118 | @Service() 119 | class ExampleClass { 120 | @Inject() 121 | inferredPropertyInjection: InjectedExampleClass; 122 | 123 | /** 124 | * We tell TypeDI that initialize the `BetterInjectedClass` class 125 | * regardless of what is the inferred type. 126 | */ 127 | @Inject(() => BetterInjectedClass) 128 | explicitPropertyInjection: InjectedExampleClass; 129 | 130 | constructor( 131 | public inferredArgumentInjection: InjectedExampleClass, 132 | /** 133 | * We tell TypeDI that initialize the `BetterInjectedClass` class 134 | * regardless of what is the inferred type. 135 | */ 136 | @Inject(() => BetterInjectedClass) 137 | public explicitArgumentInjection: InjectedExampleClass 138 | ) {} 139 | } 140 | 141 | /** 142 | * The `instance` variable is an ExampleClass instance with both the 143 | * - `inferredPropertyInjection` and `inferredArgumentInjection` property 144 | * containing an `InjectedExampleClass` instance 145 | * - `explicitPropertyInjection` and `explicitArgumentInjection` property 146 | * containing a `BetterInjectedClass` instance. 147 | */ 148 | const instance = Container.get(ExampleClass); 149 | 150 | instance.inferredPropertyInjection.print(); 151 | // prints "I am alive!" (InjectedExampleClass.print function) 152 | instance.explicitPropertyInjection.print(); 153 | // prints "I am a different class!" (BetterInjectedClass.print function) 154 | instance.inferredArgumentInjection.print(); 155 | // prints "I am alive!" (InjectedExampleClass.print function) 156 | instance.explicitArgumentInjection.print(); 157 | // prints "I am a different class!" (BetterInjectedClass.print function) 158 | ``` 159 | -------------------------------------------------------------------------------- /docs/typescript/06-service-tokens.md: -------------------------------------------------------------------------------- 1 | # Service Tokens 2 | 3 | Service tokens are unique identifiers what provides type-safe access to a value stored in a `Container`. 4 | 5 | ```ts 6 | import 'reflect-metadata'; 7 | import { Container, Token } from 'typedi'; 8 | 9 | export const JWT_SECRET_TOKEN = new Token('MY_SECRET'); 10 | 11 | Container.set(JWT_SECRET_TOKEN, 'wow-such-secure-much-encryption'); 12 | 13 | /** 14 | * Somewhere else in the application after the JWT_SECRET_TOKEN is 15 | * imported in can be used to request the secret from the Container. 16 | * 17 | * This value is type-safe also because the Token is typed. 18 | */ 19 | const JWT_SECRET = Container.get(JWT_SECRET_TOKEN); 20 | ``` 21 | 22 | ## Injecting service tokens 23 | 24 | They can be used with the `@Inject()` decorator to overwrite the inferred type of the property or argument. 25 | 26 | ```ts 27 | import 'reflect-metadata'; 28 | import { Container, Token, Inject, Service } from 'typedi'; 29 | 30 | export const JWT_SECRET_TOKEN = new Token('MY_SECRET'); 31 | 32 | Container.set(JWT_SECRET_TOKEN, 'wow-such-secure-much-encryption'); 33 | 34 | @Service() 35 | class Example { 36 | @Inject(JWT_SECRET_TOKEN) 37 | myProp: string; 38 | } 39 | 40 | const instance = Container.get(Example); 41 | // The instance.myProp property has the value assigned for the Token 42 | ``` 43 | 44 | ## Tokens with same name 45 | 46 | Two token **with the same name are different tokens**. The name is only used to help the developer identify the tokens 47 | during debugging and development. (It's included in error the messages.) 48 | 49 | ```ts 50 | import 'reflect-metadata'; 51 | import { Container, Token } from 'typedi'; 52 | 53 | const tokenA = new Token('TOKEN'); 54 | const tokenB = new Token('TOKEN'); 55 | 56 | Container.set(tokenA, 'value-A'); 57 | Container.set(tokenB, 'value-B'); 58 | 59 | const tokenValueA = Container.get(tokenA); 60 | // tokenValueA is "value-A" 61 | const tokenValueB = Container.get(tokenB); 62 | // tokenValueB is "value-B" 63 | 64 | console.log(tokenValueA === tokenValueB); 65 | // returns false, as Tokens are always unique 66 | ``` 67 | 68 | ## Difference between Token and string identifier 69 | 70 | They both achieve the same goal, however, it's recommended to use `Tokens` as they are type-safe and cannot be mistyped, 71 | while a mistyped string identifier will silently return `undefined` as value by default. 72 | -------------------------------------------------------------------------------- /docs/typescript/07-inheritance.md: -------------------------------------------------------------------------------- 1 | # Inheritance 2 | 3 | Inheritance is supported **for properties** when both the base and the extended class is marked with the `@Service()` decorator. 4 | Classes which extend a class with decorated properties will receive the initialized class instances on those properties upon creation. 5 | 6 | ```ts 7 | import 'reflect-metadata'; 8 | import { Container, Token, Inject, Service } from 'typedi'; 9 | 10 | @Service() 11 | class InjectedClass { 12 | name: string = 'InjectedClass'; 13 | } 14 | 15 | @Service() 16 | class BaseClass { 17 | name: string = 'BaseClass'; 18 | 19 | @Inject() 20 | injectedClass: InjectedClass; 21 | } 22 | 23 | @Service() 24 | class ExtendedClass extends BaseClass { 25 | name: string = 'ExtendedClass'; 26 | } 27 | 28 | const instance = Container.get(ExtendedClass); 29 | // instance has the `name` property with "ExtendedClass" value (overwritten the base class) 30 | // and the `injectedClass` property with the instance of the `InjectedClass` class 31 | 32 | console.log(instance.injectedClass.name); 33 | // logs "InjectedClass" 34 | console.log(instance.name); 35 | // logs "ExtendedClass" 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/typescript/07-usage-with-typeorm.md: -------------------------------------------------------------------------------- 1 | # Usage with TypeORM and routing-controllers 2 | 3 | To use TypeDI with routing-controllers and/or TypeORM, it's required to configure them to use the top-level 4 | TypeDI container used by your application. 5 | 6 | ```ts 7 | import { useContainer as rcUseContainer } from 'routing-controllers'; 8 | import { useContainer as typeOrmUseContainer } from 'typeorm'; 9 | import { Container } from 'typedi'; 10 | 11 | rcUseContainer(Container); 12 | typeOrmUseContainer(Container); 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/typescript/08-custom-decorators.md: -------------------------------------------------------------------------------- 1 | # Creating custom decorators 2 | 3 | > **NOTE:** This page is a direct copy of the old documentation. It will be reworked. 4 | 5 | You can create your own decorators which will inject your given values for your service dependencies. For example: 6 | 7 | ```ts 8 | // Logger.ts 9 | export function Logger() { 10 | return function (object: Object, propertyName: string, index?: number) { 11 | const logger = new ConsoleLogger(); 12 | Container.registerHandler({ object, propertyName, index, value: containerInstance => logger }); 13 | }; 14 | } 15 | 16 | // LoggerInterface.ts 17 | export interface LoggerInterface { 18 | log(message: string): void; 19 | } 20 | 21 | // ConsoleLogger.ts 22 | import { LoggerInterface } from './LoggerInterface'; 23 | 24 | export class ConsoleLogger implements LoggerInterface { 25 | log(message: string) { 26 | console.log(message); 27 | } 28 | } 29 | 30 | // UserRepository.ts 31 | @Service() 32 | export class UserRepository { 33 | constructor(@Logger() private logger: LoggerInterface) {} 34 | 35 | save(user: User) { 36 | this.logger.log(`user ${user.firstName} ${user.secondName} has been saved.`); 37 | } 38 | } 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/typescript/09-using-scoped-containers.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typestack/typedi/7c6ac12a3de8507b1306f761942562f15375ed1f/docs/typescript/09-using-scoped-containers.md -------------------------------------------------------------------------------- /docs/typescript/10-using-transient-services.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typestack/typedi/7c6ac12a3de8507b1306f761942562f15375ed1f/docs/typescript/10-using-transient-services.md -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | collectCoverageFrom: ['src/**/*.ts', '!src/**/index.ts', '!src/**/*.interface.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: 'tsconfig.spec.json', 8 | }, 9 | }, 10 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typedi", 3 | "version": "0.10.0", 4 | "description": "Dependency injection for TypeScript.", 5 | "author": "TypeStack contributors", 6 | "license": "MIT", 7 | "sideEffects": false, 8 | "main": "./cjs/index.js", 9 | "module": "./esm5/index.js", 10 | "es2015": "./esm2015/index.js", 11 | "typings": "./types/index.d.ts", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/pleerock/typedi.git" 15 | }, 16 | "tags": [ 17 | "di", 18 | "container", 19 | "di-container", 20 | "typescript", 21 | "typescript-di", 22 | "dependency-injection" 23 | ], 24 | "scripts": { 25 | "build": "npm run build:cjs", 26 | "build:clean": "rimraf build", 27 | "build:es2015": "tsc --project tsconfig.prod.esm2015.json", 28 | "build:esm5": "tsc --project tsconfig.prod.esm5.json", 29 | "build:cjs": "tsc --project tsconfig.prod.cjs.json", 30 | "build:umd": "rollup --config rollup.config.js", 31 | "build:types": "tsc --project tsconfig.prod.types.json", 32 | "prettier:fix": "prettier --write \"**/*.{ts,md}\"", 33 | "prettier:check": "prettier --check \"**/*.{ts,md}\"", 34 | "lint:fix": "eslint --max-warnings 0 --fix --ext .ts src/", 35 | "lint:check": "eslint --max-warnings 0 --ext .ts src/", 36 | "test": "jest --coverage --verbose", 37 | "test:watch": "jest --watch", 38 | "test:ci": "jest --runInBand --coverage --verbose" 39 | }, 40 | "husky": { 41 | "hooks": { 42 | "pre-commit": "lint-staged" 43 | } 44 | }, 45 | "lint-staged": { 46 | "*.md": [ 47 | "npm run prettier:fix" 48 | ], 49 | "*.ts": [ 50 | "npm run prettier:fix" 51 | ] 52 | }, 53 | "devDependencies": { 54 | "@rollup/plugin-commonjs": "^26.0.1", 55 | "@rollup/plugin-node-resolve": "^15.2.3", 56 | "@types/jest": "^27.5.0", 57 | "@types/node": "^22.5.1", 58 | "@typescript-eslint/eslint-plugin": "^5.62.0", 59 | "@typescript-eslint/parser": "^5.62.0", 60 | "eslint": "^8.57.0", 61 | "eslint-config-prettier": "^9.1.0", 62 | "eslint-plugin-jest": "^27.9.0", 63 | "husky": "^4.3.8", 64 | "jest": "^27.5.1", 65 | "lint-staged": "^15.2.9", 66 | "prettier": "^2.8.8", 67 | "reflect-metadata": "0.2.2", 68 | "rimraf": "6.0.1", 69 | "rollup": "^2.79.1", 70 | "rollup-plugin-terser": "^7.0.2", 71 | "ts-jest": "^27.1.4", 72 | "ts-node": "^10.9.2", 73 | "typescript": "^4.9.5" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import { terser } from 'rollup-plugin-terser'; 4 | 5 | export default { 6 | input: 'build/esm5/index.js', 7 | output: [ 8 | { 9 | name: 'ClassTransformer', 10 | format: 'umd', 11 | file: 'build/bundles/typedi.umd.js', 12 | sourcemap: true, 13 | }, 14 | { 15 | name: 'ClassTransformer', 16 | format: 'umd', 17 | file: 'build/bundles/typedi.umd.min.js', 18 | sourcemap: true, 19 | plugins: [terser()], 20 | }, 21 | ], 22 | plugins: [commonjs(), nodeResolve()], 23 | }; -------------------------------------------------------------------------------- /src/container-instance.class.ts: -------------------------------------------------------------------------------- 1 | import { ServiceNotFoundError } from './error/service-not-found.error'; 2 | import { CannotInstantiateValueError } from './error/cannot-instantiate-value.error'; 3 | import { Token } from './token.class'; 4 | import { Constructable } from './types/constructable.type'; 5 | import { ServiceIdentifier } from './types/service-identifier.type'; 6 | import { ServiceMetadata } from './interfaces/service-metadata.interface'; 7 | import { ServiceOptions } from './interfaces/service-options.interface'; 8 | import { EMPTY_VALUE } from './empty.const'; 9 | import { ContainerIdentifier } from './types/container-identifier.type'; 10 | import { Handler } from './interfaces/handler.interface'; 11 | import { ContainerRegistry } from './container-registry.class'; 12 | import { ContainerScope } from './types/container-scope.type'; 13 | 14 | /** 15 | * TypeDI can have multiple containers. 16 | * One container is ContainerInstance. 17 | */ 18 | export class ContainerInstance { 19 | /** Container instance id. */ 20 | public readonly id!: ContainerIdentifier; 21 | 22 | /** Metadata for all registered services in this container. */ 23 | private metadataMap: Map> = new Map(); 24 | 25 | /** 26 | * Services registered with 'multiple: true' are saved as simple services 27 | * with a generated token and the mapping between the original ID and the 28 | * generated one is stored here. This is handled like this to allow simplifying 29 | * the inner workings of the service instance. 30 | */ 31 | private multiServiceIds: Map[]; scope: ContainerScope }> = new Map(); 32 | 33 | /** 34 | * All registered handlers. The @Inject() decorator uses handlers internally to mark a property for injection. 35 | **/ 36 | private readonly handlers: Handler[] = []; 37 | 38 | /** 39 | * Indicates if the container has been disposed or not. 40 | * Any function call should fail when called after being disposed. 41 | * 42 | * NOTE: Currently not in used 43 | */ 44 | private disposed: boolean = false; 45 | 46 | constructor(id: ContainerIdentifier) { 47 | this.id = id; 48 | 49 | ContainerRegistry.registerContainer(this); 50 | 51 | /** 52 | * TODO: This is to replicate the old functionality. This should be copied only 53 | * TODO: if the container decides to inherit registered classes from a parent container. 54 | */ 55 | this.handlers = ContainerRegistry.defaultContainer?.handlers || []; 56 | } 57 | 58 | /** 59 | * Checks if the service with given name or type is registered service container. 60 | * Optionally, parameters can be passed in case if instance is initialized in the container for the first time. 61 | */ 62 | public has(identifier: ServiceIdentifier): boolean { 63 | this.throwIfDisposed(); 64 | 65 | return !!this.metadataMap.has(identifier) || !!this.multiServiceIds.has(identifier); 66 | } 67 | 68 | /** 69 | * Retrieves the service with given name or type from the service container. 70 | * Optionally, parameters can be passed in case if instance is initialized in the container for the first time. 71 | */ 72 | public get(identifier: ServiceIdentifier): T { 73 | this.throwIfDisposed(); 74 | 75 | const global = ContainerRegistry.defaultContainer.metadataMap.get(identifier); 76 | const local = this.metadataMap.get(identifier); 77 | /** If the service is registered as global we load it from there, otherwise we use the local one. */ 78 | const metadata = global?.scope === 'singleton' ? global : local; 79 | 80 | /** This should never happen as multi services are masked with custom token in Container.set. */ 81 | if (metadata && metadata.multiple === true) { 82 | throw new Error(`Cannot resolve multiple values for ${identifier.toString()} service!`); 83 | } 84 | 85 | /** Otherwise it's returned from the current container. */ 86 | if (metadata) { 87 | return this.getServiceValue(metadata); 88 | } 89 | 90 | /** 91 | * If it's the first time requested in the child container we load it from parent and set it. 92 | * TODO: This will be removed with the container inheritance rework. 93 | */ 94 | if (global && this !== ContainerRegistry.defaultContainer) { 95 | const clonedService = { ...global }; 96 | clonedService.value = EMPTY_VALUE; 97 | 98 | /** 99 | * We need to immediately set the empty value from the root container 100 | * to prevent infinite lookup in cyclic dependencies. 101 | */ 102 | this.set(clonedService); 103 | 104 | const value = this.getServiceValue(clonedService); 105 | this.set({ ...clonedService, value }); 106 | 107 | return value; 108 | } 109 | 110 | throw new ServiceNotFoundError(identifier); 111 | } 112 | 113 | /** 114 | * Gets all instances registered in the container of the given service identifier. 115 | * Used when service defined with multiple: true flag. 116 | */ 117 | public getMany(identifier: ServiceIdentifier): T[] { 118 | this.throwIfDisposed(); 119 | 120 | const globalIdMap = ContainerRegistry.defaultContainer.multiServiceIds.get(identifier); 121 | const localIdMap = this.multiServiceIds.get(identifier); 122 | 123 | /** 124 | * If the service is registered as singleton we load it from default 125 | * container, otherwise we use the local one. 126 | */ 127 | if (globalIdMap?.scope === 'singleton') { 128 | return globalIdMap.tokens.map(generatedId => ContainerRegistry.defaultContainer.get(generatedId)); 129 | } 130 | 131 | if (localIdMap) { 132 | return localIdMap.tokens.map(generatedId => this.get(generatedId)); 133 | } 134 | 135 | throw new ServiceNotFoundError(identifier); 136 | } 137 | 138 | /** 139 | * Sets a value for the given type or service name in the container. 140 | */ 141 | public set(serviceOptions: ServiceOptions): this { 142 | this.throwIfDisposed(); 143 | 144 | /** 145 | * If the service is marked as singleton, we set it in the default container. 146 | * (And avoid an infinite loop via checking if we are in the default container or not.) 147 | */ 148 | if (serviceOptions.scope === 'singleton' && ContainerRegistry.defaultContainer !== this) { 149 | ContainerRegistry.defaultContainer.set(serviceOptions); 150 | 151 | return this; 152 | } 153 | 154 | const newMetadata: ServiceMetadata = { 155 | /** 156 | * Typescript cannot understand that if ID doesn't exists then type must exists based on the 157 | * typing so we need to explicitly cast this to a `ServiceIdentifier` 158 | */ 159 | id: ((serviceOptions as any).id || (serviceOptions as any).type) as ServiceIdentifier, 160 | type: (serviceOptions as ServiceMetadata).type || null, 161 | factory: (serviceOptions as ServiceMetadata).factory, 162 | value: (serviceOptions as ServiceMetadata).value || EMPTY_VALUE, 163 | multiple: serviceOptions.multiple || false, 164 | eager: serviceOptions.eager || false, 165 | scope: serviceOptions.scope || 'container', 166 | /** We allow overriding the above options via the received config object. */ 167 | ...serviceOptions, 168 | referencedBy: new Map().set(this.id, this), 169 | }; 170 | 171 | /** If the incoming metadata is marked as multiple we mask the ID and continue saving as single value. */ 172 | if (serviceOptions.multiple) { 173 | const maskedToken = new Token(`MultiMaskToken-${newMetadata.id.toString()}`); 174 | const existingMultiGroup = this.multiServiceIds.get(newMetadata.id); 175 | 176 | if (existingMultiGroup) { 177 | existingMultiGroup.tokens.push(maskedToken); 178 | } else { 179 | this.multiServiceIds.set(newMetadata.id, { scope: newMetadata.scope, tokens: [maskedToken] }); 180 | } 181 | 182 | /** 183 | * We mask the original metadata with this generated ID, mark the service 184 | * as and continue multiple: false and continue. Marking it as 185 | * non-multiple is important otherwise Container.get would refuse to 186 | * resolve the value. 187 | */ 188 | newMetadata.id = maskedToken; 189 | newMetadata.multiple = false; 190 | } 191 | 192 | const existingMetadata = this.metadataMap.get(newMetadata.id); 193 | 194 | if (existingMetadata) { 195 | /** Service already exists, we overwrite it. (This is legacy behavior.) */ 196 | // TODO: Here we should differentiate based on the received set option. 197 | Object.assign(existingMetadata, newMetadata); 198 | } else { 199 | /** This service hasn't been registered yet, so we register it. */ 200 | this.metadataMap.set(newMetadata.id, newMetadata); 201 | } 202 | 203 | /** 204 | * If the service is eager, we need to create an instance immediately except 205 | * when the service is also marked as transient. In that case we ignore 206 | * the eager flag to prevent creating a service what cannot be disposed later. 207 | */ 208 | if (newMetadata.eager && newMetadata.scope !== 'transient') { 209 | this.get(newMetadata.id); 210 | } 211 | 212 | return this; 213 | } 214 | 215 | /** 216 | * Removes services with a given service identifiers. 217 | */ 218 | public remove(identifierOrIdentifierArray: ServiceIdentifier | ServiceIdentifier[]): this { 219 | this.throwIfDisposed(); 220 | 221 | if (Array.isArray(identifierOrIdentifierArray)) { 222 | identifierOrIdentifierArray.forEach(id => this.remove(id)); 223 | } else { 224 | const serviceMetadata = this.metadataMap.get(identifierOrIdentifierArray); 225 | 226 | if (serviceMetadata) { 227 | this.disposeServiceInstance(serviceMetadata); 228 | this.metadataMap.delete(identifierOrIdentifierArray); 229 | } 230 | } 231 | 232 | return this; 233 | } 234 | 235 | /** 236 | * Gets a separate container instance for the given instance id. 237 | */ 238 | public of(containerId: ContainerIdentifier = 'default'): ContainerInstance { 239 | this.throwIfDisposed(); 240 | 241 | if (containerId === 'default') { 242 | return ContainerRegistry.defaultContainer; 243 | } 244 | 245 | let container: ContainerInstance; 246 | 247 | if (ContainerRegistry.hasContainer(containerId)) { 248 | container = ContainerRegistry.getContainer(containerId); 249 | } else { 250 | /** 251 | * This is deprecated functionality, for now we create the container if it's doesn't exists. 252 | * This will be reworked when container inheritance is reworked. 253 | */ 254 | container = new ContainerInstance(containerId); 255 | } 256 | 257 | return container; 258 | } 259 | 260 | /** 261 | * Registers a new handler. 262 | */ 263 | public registerHandler(handler: Handler): ContainerInstance { 264 | this.handlers.push(handler); 265 | return this; 266 | } 267 | 268 | /** 269 | * Helper method that imports given services. 270 | */ 271 | /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ 272 | public import(services: Function[]): ContainerInstance { 273 | this.throwIfDisposed(); 274 | 275 | return this; 276 | } 277 | 278 | /** 279 | * Completely resets the container by removing all previously registered services from it. 280 | */ 281 | public reset(options: { strategy: 'resetValue' | 'resetServices' } = { strategy: 'resetValue' }): this { 282 | this.throwIfDisposed(); 283 | 284 | switch (options.strategy) { 285 | case 'resetValue': 286 | this.metadataMap.forEach(service => this.disposeServiceInstance(service)); 287 | break; 288 | case 'resetServices': 289 | this.metadataMap.forEach(service => this.disposeServiceInstance(service)); 290 | this.metadataMap.clear(); 291 | this.multiServiceIds.clear(); 292 | break; 293 | default: 294 | throw new Error('Received invalid reset strategy.'); 295 | } 296 | return this; 297 | } 298 | 299 | public async dispose(): Promise { 300 | this.reset({ strategy: 'resetServices' }); 301 | 302 | /** We mark the container as disposed, forbidding any further interaction with it. */ 303 | this.disposed = true; 304 | 305 | /** 306 | * Placeholder, this function returns a promise in preparation to support async services. 307 | */ 308 | await Promise.resolve(); 309 | } 310 | 311 | private throwIfDisposed() { 312 | if (this.disposed) { 313 | // TODO: Use custom error. 314 | throw new Error('Cannot use container after it has been disposed.'); 315 | } 316 | } 317 | 318 | /** 319 | * Gets the value belonging to passed in `ServiceMetadata` instance. 320 | * 321 | * - if `serviceMetadata.value` is already set it is immediately returned 322 | * - otherwise the requested type is resolved to the value saved to `serviceMetadata.value` and returned 323 | */ 324 | private getServiceValue(serviceMetadata: ServiceMetadata): any { 325 | let value: unknown = EMPTY_VALUE; 326 | 327 | /** 328 | * If the service value has been set to anything prior to this call we return that value. 329 | * NOTE: This part builds on the assumption that transient dependencies has no value set ever. 330 | */ 331 | if (serviceMetadata.value !== EMPTY_VALUE) { 332 | return serviceMetadata.value; 333 | } 334 | 335 | /** If both factory and type is missing, we cannot resolve the requested ID. */ 336 | if (!serviceMetadata.factory && !serviceMetadata.type) { 337 | throw new CannotInstantiateValueError(serviceMetadata.id); 338 | } 339 | 340 | /** 341 | * If a factory is defined it takes priority over creating an instance via `new`. 342 | * The return value of the factory is not checked, we believe by design that the user knows what he/she is doing. 343 | */ 344 | if (serviceMetadata.factory) { 345 | /** 346 | * If we received the factory in the [Constructable, "functionName"] format, we need to create the 347 | * factory first and then call the specified function on it. 348 | */ 349 | if (serviceMetadata.factory instanceof Array) { 350 | let factoryInstance; 351 | 352 | try { 353 | /** Try to get the factory from TypeDI first, if failed, fall back to simply initiating the class. */ 354 | factoryInstance = this.get(serviceMetadata.factory[0]); 355 | } catch (error) { 356 | if (error instanceof ServiceNotFoundError) { 357 | factoryInstance = new serviceMetadata.factory[0](); 358 | } else { 359 | throw error; 360 | } 361 | } 362 | 363 | value = factoryInstance[serviceMetadata.factory[1]](this, serviceMetadata.id); 364 | } else { 365 | /** If only a simple function was provided we simply call it. */ 366 | value = serviceMetadata.factory(this, serviceMetadata.id); 367 | } 368 | } 369 | 370 | /** 371 | * If no factory was provided and only then, we create the instance from the type if it was set. 372 | */ 373 | if (!serviceMetadata.factory && serviceMetadata.type) { 374 | const constructableTargetType: Constructable = serviceMetadata.type; 375 | // setup constructor parameters for a newly initialized service 376 | const paramTypes: unknown[] = (Reflect as any)?.getMetadata('design:paramtypes', constructableTargetType) || []; 377 | const params = this.initializeParams(constructableTargetType, paramTypes); 378 | 379 | // "extra feature" - always pass container instance as the last argument to the service function 380 | // this allows us to support javascript where we don't have decorators and emitted metadata about dependencies 381 | // need to be injected, and user can use provided container to get instances he needs 382 | params.push(this); 383 | 384 | value = new constructableTargetType(...params); 385 | 386 | // TODO: Calling this here, leads to infinite loop, because @Inject decorator registerds a handler 387 | // TODO: which calls Container.get, which will check if the requested type has a value set and if not 388 | // TODO: it will start the instantiation process over. So this is currently called outside of the if branch 389 | // TODO: after the current value has been assigned to the serviceMetadata. 390 | // this.applyPropertyHandlers(constructableTargetType, value as Constructable); 391 | } 392 | 393 | /** If this is not a transient service, and we resolved something, then we set it as the value. */ 394 | if (serviceMetadata.scope !== 'transient' && value !== EMPTY_VALUE) { 395 | serviceMetadata.value = value; 396 | } 397 | 398 | if (value === EMPTY_VALUE) { 399 | /** This branch should never execute, but better to be safe than sorry. */ 400 | throw new CannotInstantiateValueError(serviceMetadata.id); 401 | } 402 | 403 | if (serviceMetadata.type) { 404 | this.applyPropertyHandlers(serviceMetadata.type, value as Record); 405 | } 406 | 407 | return value; 408 | } 409 | 410 | /** 411 | * Initializes all parameter types for a given target service class. 412 | */ 413 | private initializeParams(target: Function, paramTypes: any[]): unknown[] { 414 | return paramTypes.map((paramType, index) => { 415 | const paramHandler = 416 | this.handlers.find(handler => { 417 | /** 418 | * @Inject()-ed values are stored as parameter handlers and they reference their target 419 | * when created. So when a class is extended the @Inject()-ed values are not inherited 420 | * because the handler still points to the old object only. 421 | * 422 | * As a quick fix a single level parent lookup is added via `Object.getPrototypeOf(target)`, 423 | * however this should be updated to a more robust solution. 424 | * 425 | * TODO: Add proper inheritance handling: either copy the handlers when a class is registered what 426 | * TODO: has it's parent already registered as dependency or make the lookup search up to the base Object. 427 | */ 428 | return handler.object === target && handler.index === index; 429 | }) || 430 | this.handlers.find(handler => { 431 | return handler.object === Object.getPrototypeOf(target) && handler.index === index; 432 | }); 433 | 434 | if (paramHandler) return paramHandler.value(this); 435 | 436 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 437 | if (paramType && paramType.name && !this.isPrimitiveParamType(paramType.name)) { 438 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 439 | return this.get(paramType); 440 | } 441 | 442 | return undefined; 443 | }); 444 | } 445 | 446 | /** 447 | * Checks if given parameter type is primitive type or not. 448 | */ 449 | private isPrimitiveParamType(paramTypeName: string): boolean { 450 | return ['string', 'boolean', 'number', 'object'].includes(paramTypeName.toLowerCase()); 451 | } 452 | 453 | /** 454 | * Applies all registered handlers on a given target class. 455 | */ 456 | private applyPropertyHandlers(target: Function, instance: { [key: string]: any }) { 457 | this.handlers.forEach(handler => { 458 | if (typeof handler.index === 'number') return; 459 | if (handler.object.constructor !== target && !(target.prototype instanceof handler.object.constructor)) return; 460 | 461 | if (handler.propertyName) { 462 | instance[handler.propertyName] = handler.value(this); 463 | } 464 | }); 465 | } 466 | 467 | /** 468 | * Checks if the given service metadata contains a destroyable service instance and destroys it in place. If the service 469 | * contains a callable function named `destroy` it is called but not awaited and the return value is ignored.. 470 | * 471 | * @param serviceMetadata the service metadata containing the instance to destroy 472 | * @param force when true the service will be always destroyed even if it's cannot be re-created 473 | */ 474 | private disposeServiceInstance(serviceMetadata: ServiceMetadata, force = false) { 475 | this.throwIfDisposed(); 476 | 477 | /** We reset value only if we can re-create it (aka type or factory exists). */ 478 | const shouldResetValue = force || !!serviceMetadata.type || !!serviceMetadata.factory; 479 | 480 | if (shouldResetValue) { 481 | /** If we wound a function named destroy we call it without any params. */ 482 | if (typeof (serviceMetadata?.value as Record)['dispose'] === 'function') { 483 | try { 484 | (serviceMetadata.value as { dispose: CallableFunction }).dispose(); 485 | } catch (error) { 486 | /** We simply ignore the errors from the destroy function. */ 487 | } 488 | } 489 | 490 | serviceMetadata.value = EMPTY_VALUE; 491 | } 492 | } 493 | } 494 | -------------------------------------------------------------------------------- /src/container-registry.class.ts: -------------------------------------------------------------------------------- 1 | import { ContainerInstance } from './container-instance.class'; 2 | import { ContainerIdentifier } from './types/container-identifier.type'; 3 | 4 | /** 5 | * The container registry is responsible for holding the default and every 6 | * created container instance for later access. 7 | * 8 | * _Note: This class is for internal use and it's API may break in minor or 9 | * patch releases without warning._ 10 | */ 11 | export class ContainerRegistry { 12 | /** 13 | * The list of all known container. Created containers are automatically added 14 | * to this list. Two container cannot be registered with the same ID. 15 | * 16 | * This map doesn't contains the default container. 17 | */ 18 | private static readonly containerMap: Map = new Map(); 19 | 20 | /** 21 | * The default global container. By default services are registered into this 22 | * container when registered via `Container.set()` or `@Service` decorator. 23 | */ 24 | public static readonly defaultContainer: ContainerInstance = new ContainerInstance('default'); 25 | 26 | /** 27 | * Registers the given container instance or throws an error. 28 | * 29 | * _Note: This function is auto-called when a Container instance is created, 30 | * it doesn't need to be called manually!_ 31 | * 32 | * @param container the container to add to the registry 33 | */ 34 | public static registerContainer(container: ContainerInstance): void { 35 | if (container instanceof ContainerInstance === false) { 36 | // TODO: Create custom error for this. 37 | throw new Error('Only ContainerInstance instances can be registered.'); 38 | } 39 | 40 | /** If we already set the default container (in index) then no-one else can register a default. */ 41 | if (!!ContainerRegistry.defaultContainer && container.id === 'default') { 42 | // TODO: Create custom error for this. 43 | throw new Error('You cannot register a container with the "default" ID.'); 44 | } 45 | 46 | if (ContainerRegistry.containerMap.has(container.id)) { 47 | // TODO: Create custom error for this. 48 | throw new Error('Cannot register container with same ID.'); 49 | } 50 | 51 | ContainerRegistry.containerMap.set(container.id, container); 52 | } 53 | 54 | /** 55 | * Returns true if a container exists with the given ID or false otherwise. 56 | * 57 | * @param container the ID of the container 58 | */ 59 | public static hasContainer(id: ContainerIdentifier): boolean { 60 | return ContainerRegistry.containerMap.has(id); 61 | } 62 | 63 | /** 64 | * Returns the container for requested ID or throws an error if no container 65 | * is registered with the given ID. 66 | * 67 | * @param container the ID of the container 68 | */ 69 | public static getContainer(id: ContainerIdentifier): ContainerInstance { 70 | const registeredContainer = this.containerMap.get(id); 71 | 72 | if (registeredContainer === undefined) { 73 | // TODO: Create custom error for this. 74 | throw new Error('No container is registered with the given ID.'); 75 | } 76 | 77 | return registeredContainer; 78 | } 79 | 80 | /** 81 | * Removes the given container from the registry and disposes all services 82 | * registered only in this container. 83 | * 84 | * This function throws an error if no 85 | * - container exists with the given ID 86 | * - any of the registered services threw an error during it's disposal 87 | * 88 | * @param container the container to remove from the registry 89 | */ 90 | public static async removeContainer(container: ContainerInstance): Promise { 91 | const registeredContainer = ContainerRegistry.containerMap.get(container.id); 92 | 93 | if (registeredContainer === undefined) { 94 | // TODO: Create custom error for this. 95 | throw new Error('No container is registered with the given ID.'); 96 | } 97 | 98 | /** We remove the container first. */ 99 | ContainerRegistry.containerMap.delete(container.id); 100 | 101 | /** We dispose all registered classes in the container. */ 102 | await registeredContainer.dispose(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/decorators/inject-many.decorator.ts: -------------------------------------------------------------------------------- 1 | import { ContainerRegistry } from '../container-registry.class'; 2 | import { Token } from '../token.class'; 3 | import { CannotInjectValueError } from '../error/cannot-inject-value.error'; 4 | import { resolveToTypeWrapper } from '../utils/resolve-to-type-wrapper.util'; 5 | import { Constructable } from '../types/constructable.type'; 6 | import { ServiceIdentifier } from '../types/service-identifier.type'; 7 | 8 | /** 9 | * Injects a list of services into a class property or constructor parameter. 10 | */ 11 | export function InjectMany(): Function; 12 | export function InjectMany(type?: (type?: any) => Function): Function; 13 | export function InjectMany(serviceName?: string): Function; 14 | export function InjectMany(token: Token): Function; 15 | export function InjectMany( 16 | typeOrIdentifier?: ((type?: never) => Constructable) | ServiceIdentifier 17 | ): Function { 18 | return function (target: Object, propertyName: string | Symbol, index?: number): void { 19 | const typeWrapper = resolveToTypeWrapper(typeOrIdentifier, target, propertyName, index); 20 | 21 | /** If no type was inferred, or the general Object type was inferred we throw an error. */ 22 | if (typeWrapper === undefined || typeWrapper.eagerType === undefined || typeWrapper.eagerType === Object) { 23 | throw new CannotInjectValueError(target as Constructable, propertyName as string); 24 | } 25 | 26 | ContainerRegistry.defaultContainer.registerHandler({ 27 | object: target as Constructable, 28 | propertyName: propertyName as string, 29 | index: index, 30 | value: containerInstance => { 31 | const evaluatedLazyType = typeWrapper.lazyType(); 32 | 33 | /** If no type was inferred lazily, or the general Object type was inferred we throw an error. */ 34 | if (evaluatedLazyType === undefined || evaluatedLazyType === Object) { 35 | throw new CannotInjectValueError(target as Constructable, propertyName as string); 36 | } 37 | 38 | return containerInstance.getMany(evaluatedLazyType); 39 | }, 40 | }); 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/decorators/inject.decorator.ts: -------------------------------------------------------------------------------- 1 | import { ContainerRegistry } from '../container-registry.class'; 2 | import { Token } from '../token.class'; 3 | import { CannotInjectValueError } from '../error/cannot-inject-value.error'; 4 | import { ServiceIdentifier } from '../types/service-identifier.type'; 5 | import { Constructable } from '../types/constructable.type'; 6 | import { resolveToTypeWrapper } from '../utils/resolve-to-type-wrapper.util'; 7 | 8 | /** 9 | * Injects a service into a class property or constructor parameter. 10 | */ 11 | export function Inject(): Function; 12 | export function Inject(typeFn: (type?: never) => Constructable): Function; 13 | export function Inject(serviceName?: string): Function; 14 | export function Inject(token: Token): Function; 15 | export function Inject( 16 | typeOrIdentifier?: ((type?: never) => Constructable) | ServiceIdentifier 17 | ): ParameterDecorator | PropertyDecorator { 18 | return function (target: Object, propertyName: string | Symbol, index?: number): void { 19 | const typeWrapper = resolveToTypeWrapper(typeOrIdentifier, target, propertyName, index); 20 | 21 | /** If no type was inferred, or the general Object type was inferred we throw an error. */ 22 | if (typeWrapper === undefined || typeWrapper.eagerType === undefined || typeWrapper.eagerType === Object) { 23 | throw new CannotInjectValueError(target as Constructable, propertyName as string); 24 | } 25 | 26 | ContainerRegistry.defaultContainer.registerHandler({ 27 | object: target as Constructable, 28 | propertyName: propertyName as string, 29 | index: index, 30 | value: containerInstance => { 31 | const evaluatedLazyType = typeWrapper.lazyType(); 32 | 33 | /** If no type was inferred lazily, or the general Object type was inferred we throw an error. */ 34 | if (evaluatedLazyType === undefined || evaluatedLazyType === Object) { 35 | throw new CannotInjectValueError(target as Constructable, propertyName as string); 36 | } 37 | 38 | return containerInstance.get(evaluatedLazyType); 39 | }, 40 | }); 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/decorators/service.decorator.ts: -------------------------------------------------------------------------------- 1 | import { ContainerRegistry } from '../container-registry.class'; 2 | import { ServiceMetadata } from '../interfaces/service-metadata.interface'; 3 | import { ServiceOptions } from '../interfaces/service-options.interface'; 4 | import { EMPTY_VALUE } from '../empty.const'; 5 | import { Constructable } from '../types/constructable.type'; 6 | 7 | /** 8 | * Marks class as a service that can be injected using Container. 9 | */ 10 | /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ 11 | export function Service(): Function; 12 | export function Service(options: ServiceOptions): Function; 13 | export function Service(options: ServiceOptions = {}): ClassDecorator { 14 | return targetConstructor => { 15 | const serviceMetadata: ServiceMetadata = { 16 | id: options.id || targetConstructor, 17 | type: targetConstructor as unknown as Constructable, 18 | factory: (options as any).factory || undefined, 19 | multiple: options.multiple || false, 20 | eager: options.eager || false, 21 | scope: options.scope || 'container', 22 | referencedBy: new Map().set(ContainerRegistry.defaultContainer.id, ContainerRegistry.defaultContainer), 23 | value: EMPTY_VALUE, 24 | }; 25 | 26 | ContainerRegistry.defaultContainer.set(serviceMetadata); 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/empty.const.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Indicates that a service has not been initialized yet. 3 | * 4 | * _Note: This value is for internal use only._ 5 | */ 6 | export const EMPTY_VALUE = Symbol('EMPTY_VALUE'); 7 | -------------------------------------------------------------------------------- /src/error/cannot-inject-value.error.ts: -------------------------------------------------------------------------------- 1 | import { Constructable } from '../types/constructable.type'; 2 | 3 | /** 4 | * Thrown when DI cannot inject value into property decorated by @Inject decorator. 5 | */ 6 | export class CannotInjectValueError extends Error { 7 | public name = 'CannotInjectValueError'; 8 | 9 | get message(): string { 10 | return ( 11 | `Cannot inject value into "${this.target.constructor.name}.${this.propertyName}". ` + 12 | `Please make sure you setup reflect-metadata properly and you don't use interfaces without service tokens as injection value.` 13 | ); 14 | } 15 | 16 | constructor(private target: Constructable, private propertyName: string) { 17 | super(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/error/cannot-instantiate-value.error.ts: -------------------------------------------------------------------------------- 1 | import { ServiceIdentifier } from '../types/service-identifier.type'; 2 | import { Token } from '../token.class'; 3 | 4 | /** 5 | * Thrown when DI cannot inject value into property decorated by @Inject decorator. 6 | */ 7 | export class CannotInstantiateValueError extends Error { 8 | public name = 'CannotInstantiateValueError'; 9 | 10 | /** Normalized identifier name used in the error message. */ 11 | private normalizedIdentifier: string = ''; 12 | 13 | get message(): string { 14 | return ( 15 | `Cannot instantiate the requested value for the "${this.normalizedIdentifier}" identifier. ` + 16 | `The related metadata doesn't contain a factory or a type to instantiate.` 17 | ); 18 | } 19 | 20 | constructor(identifier: ServiceIdentifier) { 21 | super(); 22 | 23 | // TODO: Extract this to a helper function and share between this and NotFoundError. 24 | if (typeof identifier === 'string') { 25 | this.normalizedIdentifier = identifier; 26 | } else if (identifier instanceof Token) { 27 | this.normalizedIdentifier = `Token<${identifier.name || 'UNSET_NAME'}>`; 28 | } else if (identifier && (identifier.name || identifier.prototype?.name)) { 29 | this.normalizedIdentifier = 30 | `MaybeConstructable<${identifier.name}>` || 31 | `MaybeConstructable<${(identifier.prototype as { name: string })?.name}>`; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/error/service-not-found.error.ts: -------------------------------------------------------------------------------- 1 | import { ServiceIdentifier } from '../types/service-identifier.type'; 2 | import { Token } from '../token.class'; 3 | 4 | /** 5 | * Thrown when requested service was not found. 6 | */ 7 | export class ServiceNotFoundError extends Error { 8 | public name = 'ServiceNotFoundError'; 9 | 10 | /** Normalized identifier name used in the error message. */ 11 | private normalizedIdentifier: string = ''; 12 | 13 | get message(): string { 14 | return ( 15 | `Service with "${this.normalizedIdentifier}" identifier was not found in the container. ` + 16 | `Register it before usage via explicitly calling the "Container.set" function or using the "@Service()" decorator.` 17 | ); 18 | } 19 | 20 | constructor(identifier: ServiceIdentifier) { 21 | super(); 22 | 23 | if (typeof identifier === 'string') { 24 | this.normalizedIdentifier = identifier; 25 | } else if (identifier instanceof Token) { 26 | this.normalizedIdentifier = `Token<${identifier.name || 'UNSET_NAME'}>`; 27 | } else if (identifier && (identifier.name || identifier.prototype?.name)) { 28 | this.normalizedIdentifier = 29 | `MaybeConstructable<${identifier.name}>` || 30 | `MaybeConstructable<${(identifier.prototype as { name: string })?.name}>`; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * We have a hard dependency on reflect-metadata package. Without it the 3 | * dependency lookup won't work, so we warn users when it's not loaded. 4 | */ 5 | if (!Reflect || !(Reflect as any).getMetadata) { 6 | throw new Error( 7 | 'TypeDI requires "Reflect.getMetadata" to work. Please import the "reflect-metadata" package at the very first line of your application.' 8 | ); 9 | } 10 | 11 | /** This is an internal package, so we don't re-export it on purpose. */ 12 | import { ContainerRegistry } from './container-registry.class'; 13 | 14 | export * from './decorators/inject-many.decorator'; 15 | export * from './decorators/inject.decorator'; 16 | export * from './decorators/service.decorator'; 17 | 18 | export * from './error/cannot-inject-value.error'; 19 | export * from './error/cannot-instantiate-value.error'; 20 | export * from './error/service-not-found.error'; 21 | 22 | export { Handler } from './interfaces/handler.interface'; 23 | export { ServiceMetadata } from './interfaces/service-metadata.interface'; 24 | export { ServiceOptions } from './interfaces/service-options.interface'; 25 | export { Constructable } from './types/constructable.type'; 26 | export { ServiceIdentifier } from './types/service-identifier.type'; 27 | 28 | export { ContainerInstance } from './container-instance.class'; 29 | export { Token } from './token.class'; 30 | 31 | /** We export the default container under the Container alias. */ 32 | export const Container = ContainerRegistry.defaultContainer; 33 | export default Container; 34 | -------------------------------------------------------------------------------- /src/interfaces/container-options.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ContainerOptions { 2 | /** 3 | * Controls the behavior when a service is already registered with the same ID. The following values are allowed: 4 | * 5 | * - `throw` - a `ContainerCannotBeCreatedError` error is raised 6 | * - `overwrite` - the previous container is disposed and a new one is created 7 | * - `returnExisting` - returns the existing container or raises a `ContainerCannotBeCreatedError` error if the 8 | * specified options differ from the options of the existing container 9 | * 10 | * The default value is `returnExisting`. 11 | */ 12 | onConflict: 'throw' | 'overwrite' | 'returnExisting'; 13 | 14 | /** 15 | * Controls the behavior when a requested type doesn't exists in the current container. The following values are allowed: 16 | * 17 | * - `allowLookup` - the parent container will be checked for the dependency 18 | * - `localOnly` - a `ServiceNotFoundError` error is raised 19 | * 20 | * The default value is `allowLookup`. 21 | */ 22 | lookupStrategy: 'allowLookup' | 'localOnly'; 23 | 24 | /** 25 | * Enables the lookup for global (singleton) services before checking in the current container. By default every 26 | * type is first checked in the default container to return singleton services. This check bypasses the lookup strategy, 27 | * set in the container so if this behavior is not desired it can be disabled via this flag. 28 | * 29 | * The default value is `true`. 30 | */ 31 | allowSingletonLookup: boolean; 32 | 33 | /** 34 | * Controls how the child container inherits the service definitions from it's parent. The following values are allowed: 35 | * 36 | * - `none` - no metadata is inherited 37 | * - `definitionOnly` - only metadata is inherited, a new instance will be created for each class 38 | * - eager classes created as soon as the container is created 39 | * - non-eager classes are created the first time they are requested 40 | * - `definitionWithValues` - both metadata and service instances are inherited 41 | * - when parent class is disposed the instances in this container are preserved 42 | * - if a service is registered but not created yet, it will be shared when created between the two container 43 | * - newly registered services won't be shared between the two container 44 | * 45 | * The default value is `none`. 46 | */ 47 | inheritanceStrategy: 'none' | 'definitionOnly' | 'definitionWithValues'; 48 | } 49 | -------------------------------------------------------------------------------- /src/interfaces/handler.interface.ts: -------------------------------------------------------------------------------- 1 | import { ContainerInstance } from '../container-instance.class'; 2 | import { Constructable } from '../types/constructable.type'; 3 | 4 | /** 5 | * Used to register special "handler" which will be executed on a service class during its initialization. 6 | * It can be used to create custom decorators and set/replace service class properties and constructor parameters. 7 | */ 8 | export interface Handler { 9 | /** 10 | * Service object used to apply handler to. 11 | */ 12 | object: Constructable; 13 | 14 | /** 15 | * Class property name to set/replace value of. 16 | * Used if handler is applied on a class property. 17 | */ 18 | propertyName?: string; 19 | 20 | /** 21 | * Parameter index to set/replace value of. 22 | * Used if handler is applied on a constructor parameter. 23 | */ 24 | index?: number; 25 | 26 | /** 27 | * Factory function that produces value that will be set to class property or constructor parameter. 28 | * Accepts container instance which requested the value. 29 | */ 30 | value: (container: ContainerInstance) => any; 31 | } 32 | -------------------------------------------------------------------------------- /src/interfaces/service-metadata.interface.ts: -------------------------------------------------------------------------------- 1 | import { ContainerInstance } from '../container-instance.class'; 2 | import { Constructable } from '../types/constructable.type'; 3 | import { ContainerIdentifier } from '../types/container-identifier.type'; 4 | import { ContainerScope } from '../types/container-scope.type'; 5 | import { ServiceIdentifier } from '../types/service-identifier.type'; 6 | 7 | /** 8 | * Service metadata is used to initialize service and store its state. 9 | */ 10 | export interface ServiceMetadata { 11 | /** Unique identifier of the referenced service. */ 12 | id: ServiceIdentifier; 13 | 14 | /** 15 | * The injection scope for the service. 16 | * - a `singleton` service always will be created in the default container regardless of who registering it 17 | * - a `container` scoped service will be created once when requested from the given container 18 | * - a `transient` service will be created each time it is requested 19 | */ 20 | scope: ContainerScope; 21 | 22 | /** 23 | * Class definition of the service what is used to initialize given service. 24 | * This property maybe null if the value of the service is set manually. 25 | * If id is not set then it serves as service id. 26 | */ 27 | type: Constructable | null; 28 | 29 | /** 30 | * Factory function used to initialize this service. 31 | * Can be regular function ("createCar" for example), 32 | * or other service which produces this instance ([CarFactory, "createCar"] for example). 33 | */ 34 | factory: [Constructable, string] | CallableFunction | undefined; 35 | 36 | /** 37 | * Instance of the target class. 38 | */ 39 | value: unknown | Symbol; 40 | 41 | /** 42 | * Allows to setup multiple instances the different classes under a single service id string or token. 43 | */ 44 | multiple: boolean; 45 | 46 | /** 47 | * Indicates whether a new instance should be created as soon as the class is registered. 48 | * By default the registered classes are only instantiated when they are requested from the container. 49 | * 50 | * _Note: This option is ignored for transient services._ 51 | */ 52 | eager: boolean; 53 | 54 | /** 55 | * Map of containers referencing this metadata. This is used when a container 56 | * is inheriting it's parents definitions and values to track the lifecycle of 57 | * the metadata. Namely, a service can be disposed only if it's only referenced 58 | * by the container being disposed. 59 | */ 60 | referencedBy: Map; 61 | } 62 | -------------------------------------------------------------------------------- /src/interfaces/service-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { ServiceMetadata } from './service-metadata.interface'; 2 | 3 | /** 4 | * The public ServiceOptions is partial object of ServiceMetadata and either one 5 | * of the following is set: `type`, `factory`, `value` but not more than one. 6 | */ 7 | export type ServiceOptions = 8 | | Omit>, 'referencedBy' | 'type' | 'factory'> 9 | | Omit>, 'referencedBy' | 'value' | 'factory'> 10 | | Omit>, 'referencedBy' | 'value' | 'type'>; 11 | -------------------------------------------------------------------------------- /src/token.class.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Used to create unique typed service identifier. 3 | * Useful when service has only interface, but don't have a class. 4 | */ 5 | /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ 6 | export class Token { 7 | /** 8 | * @param name Token name, optional and only used for debugging purposes. 9 | */ 10 | constructor(public name?: string) {} 11 | } 12 | -------------------------------------------------------------------------------- /src/types/abstract-constructable.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generic type for abstract class definitions. 3 | * 4 | * Explanation: This describes a newable Function with a prototype Which is 5 | * what an abstract class is - no constructor, just the prototype. 6 | */ 7 | export type AbstractConstructable = NewableFunction & { prototype: T }; 8 | -------------------------------------------------------------------------------- /src/types/constructable.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generic type for class definitions. 3 | * Example usage: 4 | * ``` 5 | * function createSomeInstance(myClassDefinition: Constructable) { 6 | * return new myClassDefinition() 7 | * } 8 | * ``` 9 | */ 10 | export type Constructable = new (...args: any[]) => T; 11 | -------------------------------------------------------------------------------- /src/types/container-identifier.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A container identifier. This value must be unique across all containers. 3 | */ 4 | export type ContainerIdentifier = string | Symbol; 5 | -------------------------------------------------------------------------------- /src/types/container-scope.type.ts: -------------------------------------------------------------------------------- 1 | export type ContainerScope = 'singleton' | 'container' | 'transient'; 2 | -------------------------------------------------------------------------------- /src/types/service-identifier.type.ts: -------------------------------------------------------------------------------- 1 | import { Token } from '../token.class'; 2 | import { Constructable } from './constructable.type'; 3 | import { AbstractConstructable } from './abstract-constructable.type'; 4 | 5 | /** 6 | * Unique service identifier. 7 | * Can be some class type, or string id, or instance of Token. 8 | */ 9 | export type ServiceIdentifier = 10 | | Constructable 11 | | AbstractConstructable 12 | | CallableFunction 13 | | Token 14 | | string; 15 | -------------------------------------------------------------------------------- /src/utils/resolve-to-type-wrapper.util.ts: -------------------------------------------------------------------------------- 1 | import { Token } from '../token.class'; 2 | import { Constructable } from '../types/constructable.type'; 3 | import { ServiceIdentifier } from '../types/service-identifier.type'; 4 | 5 | /** 6 | * Helper function used in inject decorators to resolve the received identifier to 7 | * an eager type when possible or to a lazy type when cyclic dependencies are possibly involved. 8 | * 9 | * @param typeOrIdentifier a service identifier or a function returning a type acting as service identifier or nothing 10 | * @param target the class definition of the target of the decorator 11 | * @param propertyName the name of the property in case of a PropertyDecorator 12 | * @param index the index of the parameter in the constructor in case of ParameterDecorator 13 | */ 14 | export function resolveToTypeWrapper( 15 | typeOrIdentifier: ((type?: never) => Constructable) | ServiceIdentifier | undefined, 16 | target: Object, 17 | propertyName: string | Symbol, 18 | index?: number 19 | ): { eagerType: ServiceIdentifier | null; lazyType: (type?: never) => ServiceIdentifier } { 20 | /** 21 | * ? We want to error out as soon as possible when looking up services to inject, however 22 | * ? we cannot determine the type at decorator execution when cyclic dependencies are involved 23 | * ? because calling the received `() => MyType` function right away would cause a JS error: 24 | * ? "Cannot access 'MyType' before initialization", so we need to execute the function in the handler, 25 | * ? when the classes are already created. To overcome this, we use a wrapper: 26 | * ? - the lazyType is executed in the handler so we never have a JS error 27 | * ? - the eagerType is checked when decorator is running and an error is raised if an unknown type is encountered 28 | */ 29 | let typeWrapper!: { eagerType: ServiceIdentifier | null; lazyType: (type?: never) => ServiceIdentifier }; 30 | 31 | /** If requested type is explicitly set via a string ID or token, we set it explicitly. */ 32 | if ((typeOrIdentifier && typeof typeOrIdentifier === 'string') || typeOrIdentifier instanceof Token) { 33 | typeWrapper = { eagerType: typeOrIdentifier, lazyType: () => typeOrIdentifier }; 34 | } 35 | 36 | /** If requested type is explicitly set via a () => MyClassType format, we set it explicitly. */ 37 | if (typeOrIdentifier && typeof typeOrIdentifier === 'function') { 38 | /** We set eagerType to null, preventing the raising of the CannotInjectValueError in decorators. */ 39 | typeWrapper = { eagerType: null, lazyType: () => (typeOrIdentifier as CallableFunction)() }; 40 | } 41 | 42 | /** If no explicit type is set and handler registered for a class property, we need to get the property type. */ 43 | if (!typeOrIdentifier && propertyName) { 44 | const identifier = (Reflect as any).getMetadata('design:type', target, propertyName); 45 | 46 | typeWrapper = { eagerType: identifier, lazyType: () => identifier }; 47 | } 48 | 49 | /** If no explicit type is set and handler registered for a constructor parameter, we need to get the parameter types. */ 50 | if (!typeOrIdentifier && typeof index == 'number' && Number.isInteger(index)) { 51 | const paramTypes: ServiceIdentifier[] = (Reflect as any).getMetadata('design:paramtypes', target, propertyName); 52 | /** It's not guaranteed, that we find any types for the constructor. */ 53 | const identifier = paramTypes?.[index]; 54 | 55 | typeWrapper = { eagerType: identifier, lazyType: () => identifier }; 56 | } 57 | 58 | return typeWrapper; 59 | } 60 | -------------------------------------------------------------------------------- /test/Container.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Constructable, Container } from '../src/index'; 3 | import { Service } from '../src/decorators/service.decorator'; 4 | import { Token } from '../src/token.class'; 5 | import { ServiceNotFoundError } from '../src/error/service-not-found.error'; 6 | 7 | describe('Container', function () { 8 | beforeEach(() => Container.reset({ strategy: 'resetValue' })); 9 | 10 | describe('get', () => { 11 | it('should be able to get a boolean', () => { 12 | const booleanTrue = 'boolean.true'; 13 | const booleanFalse = 'boolean.false'; 14 | Container.set({ id: booleanTrue, value: true }); 15 | Container.set({ id: booleanFalse, value: false }); 16 | 17 | expect(Container.get(booleanTrue)).toBe(true); 18 | expect(Container.get(booleanFalse)).toBe(false); 19 | }); 20 | 21 | it('should be able to get an empty string', () => { 22 | const emptyString = 'emptyString'; 23 | Container.set({ id: emptyString, value: '' }); 24 | 25 | expect(Container.get(emptyString)).toBe(''); 26 | }); 27 | 28 | it('should be able to get the 0 number', () => { 29 | const zero = 'zero'; 30 | Container.set({ id: zero, value: 0 }); 31 | 32 | expect(Container.get(zero)).toBe(0); 33 | }); 34 | }); 35 | 36 | describe('set', function () { 37 | it('should be able to set a class into the container', function () { 38 | class TestService { 39 | constructor(public name: string) {} 40 | } 41 | const testService = new TestService('this is test'); 42 | Container.set({ id: TestService, value: testService }); 43 | expect(Container.get(TestService)).toBe(testService); 44 | expect(Container.get(TestService).name).toBe('this is test'); 45 | }); 46 | 47 | it('should be able to set a named service', function () { 48 | class TestService { 49 | constructor(public name: string) {} 50 | } 51 | const firstService = new TestService('first'); 52 | Container.set({ id: 'first.service', value: firstService }); 53 | 54 | const secondService = new TestService('second'); 55 | Container.set({ id: 'second.service', value: secondService }); 56 | 57 | expect(Container.get('first.service').name).toBe('first'); 58 | expect(Container.get('second.service').name).toBe('second'); 59 | }); 60 | 61 | it('should be able to set a tokenized service', function () { 62 | class TestService { 63 | constructor(public name: string) {} 64 | } 65 | const FirstTestToken = new Token(); 66 | const SecondTestToken = new Token(); 67 | 68 | const firstService = new TestService('first'); 69 | Container.set({ id: FirstTestToken, value: firstService }); 70 | 71 | const secondService = new TestService('second'); 72 | Container.set({ id: SecondTestToken, value: secondService }); 73 | 74 | expect(Container.get(FirstTestToken).name).toBe('first'); 75 | expect(Container.get(SecondTestToken).name).toBe('second'); 76 | }); 77 | 78 | it('should override previous value if service is written second time', function () { 79 | class TestService { 80 | constructor(public name: string) {} 81 | } 82 | const TestToken = new Token(); 83 | 84 | const firstService = new TestService('first'); 85 | Container.set({ id: TestToken, value: firstService }); 86 | expect(Container.get(TestToken)).toBe(firstService); 87 | expect(Container.get(TestToken).name).toBe('first'); 88 | 89 | const secondService = new TestService('second'); 90 | Container.set({ id: TestToken, value: secondService }); 91 | 92 | expect(Container.get(TestToken)).toBe(secondService); 93 | expect(Container.get(TestToken).name).toBe('second'); 94 | }); 95 | }); 96 | 97 | describe('set multiple', function () { 98 | it('should be able to provide a list of values', function () { 99 | class TestService {} 100 | 101 | class TestServiceFactory { 102 | create() { 103 | return 'test3-service-created-by-factory'; 104 | } 105 | } 106 | 107 | const testService = new TestService(); 108 | const test1Service = new TestService(); 109 | const test2Service = new TestService(); 110 | 111 | Container.set({ id: TestService, value: testService }); 112 | Container.set({ id: 'test1-service', value: test1Service }); 113 | Container.set({ id: 'test2-service', value: test2Service }); 114 | Container.set({ id: 'test3-service', factory: [TestServiceFactory, 'create'] }); 115 | 116 | expect(Container.get(TestService)).toBe(testService); 117 | expect(Container.get('test1-service')).toBe(test1Service); 118 | expect(Container.get('test2-service')).toBe(test2Service); 119 | expect(Container.get('test3-service')).toBe('test3-service-created-by-factory'); 120 | }); 121 | }); 122 | 123 | describe('remove', function () { 124 | it('should be able to remove previously registered services', function () { 125 | class TestService { 126 | constructor() {} 127 | } 128 | 129 | const testService = new TestService(); 130 | const test1Service = new TestService(); 131 | const test2Service = new TestService(); 132 | 133 | Container.set({ id: TestService, value: testService }); 134 | Container.set({ id: 'test1-service', value: test1Service }); 135 | Container.set({ id: 'test2-service', value: test2Service }); 136 | 137 | expect(Container.get(TestService)).toBe(testService); 138 | expect(Container.get('test1-service')).toBe(test1Service); 139 | expect(Container.get('test2-service')).toBe(test2Service); 140 | 141 | Container.remove(['test1-service', 'test2-service']); 142 | 143 | expect(Container.get(TestService)).toBe(testService); 144 | expect(() => Container.get('test1-service')).toThrowError(ServiceNotFoundError); 145 | expect(() => Container.get('test2-service')).toThrowError(ServiceNotFoundError); 146 | }); 147 | }); 148 | 149 | describe('reset', function () { 150 | it('should support container reset', () => { 151 | @Service() 152 | class TestService { 153 | constructor(public name: string = 'frank') {} 154 | } 155 | 156 | Container.set({ id: TestService, type: TestService }); 157 | const testService = Container.get(TestService); 158 | testService.name = 'john'; 159 | 160 | expect(Container.get(TestService)).toBe(testService); 161 | expect(Container.get(TestService).name).toBe('john'); 162 | Container.reset({ strategy: 'resetValue' }); 163 | expect(Container.get(TestService)).not.toBe(testService); 164 | expect(Container.get(TestService).name).toBe('frank'); 165 | }); 166 | }); 167 | 168 | describe('registerHandler', function () { 169 | it('should have ability to pre-specify class initialization parameters', function () { 170 | @Service() 171 | class ExtraService { 172 | constructor(public luckyNumber: number, public message: string) {} 173 | } 174 | 175 | Container.registerHandler({ 176 | object: ExtraService, 177 | index: 0, 178 | value: containerInstance => 777, 179 | }); 180 | 181 | Container.registerHandler({ 182 | object: ExtraService, 183 | index: 1, 184 | value: containerInstance => 'hello parameter', 185 | }); 186 | 187 | expect(Container.get(ExtraService).luckyNumber).toBe(777); 188 | expect(Container.get(ExtraService).message).toBe('hello parameter'); 189 | }); 190 | 191 | it('should have ability to pre-specify initialized class properties', function () { 192 | function CustomInject(value: any) { 193 | return function (target: any, propertyName: string) { 194 | Container.registerHandler({ 195 | object: target, 196 | propertyName: propertyName, 197 | value: containerInstance => value, 198 | }); 199 | }; 200 | } 201 | 202 | @Service() 203 | class ExtraService { 204 | @CustomInject(888) 205 | badNumber: number; 206 | 207 | @CustomInject('bye world') 208 | byeMessage: string; 209 | } 210 | 211 | expect(Container.get(ExtraService).badNumber).toBe(888); 212 | expect(Container.get(ExtraService).byeMessage).toBe('bye world'); 213 | }); 214 | 215 | it('should inject the right value in subclass constructor params', function () { 216 | function CustomInject(value: any) { 217 | return function (target: Constructable, propertyName: string, index: number) { 218 | Container.registerHandler({ 219 | object: target, 220 | propertyName: propertyName, 221 | index: index, 222 | value: containerInstance => value, 223 | }); 224 | }; 225 | } 226 | 227 | @Service() 228 | class SuperService { 229 | constructor(@CustomInject(888) readonly num: number) {} 230 | } 231 | 232 | @Service() 233 | class SubService extends SuperService { 234 | constructor(@CustomInject(666) num: number) { 235 | super(num); 236 | } 237 | } 238 | 239 | expect(Container.get(SuperService).num).toBe(888); 240 | expect(Container.get(SubService).num).toBe(666); 241 | }); 242 | }); 243 | 244 | describe('set with ServiceMetadata passed', function () { 245 | it('should support factory functions', function () { 246 | class Engine { 247 | public serialNumber = 'A-123'; 248 | } 249 | 250 | class Car { 251 | constructor(public engine: Engine) {} 252 | } 253 | 254 | Container.set({ 255 | id: Car, 256 | factory: () => new Car(new Engine()), 257 | }); 258 | 259 | expect(Container.get(Car).engine.serialNumber).toBe('A-123'); 260 | }); 261 | 262 | it('should support factory classes', function () { 263 | @Service() 264 | class Engine { 265 | public serialNumber = 'A-123'; 266 | } 267 | 268 | class Car { 269 | constructor(public engine: Engine) {} 270 | } 271 | 272 | @Service() 273 | class CarFactory { 274 | constructor(private engine: Engine) {} 275 | 276 | createCar(): Car { 277 | return new Car(this.engine); 278 | } 279 | } 280 | 281 | Container.set({ 282 | id: Car, 283 | factory: [CarFactory, 'createCar'], 284 | }); 285 | 286 | expect(Container.get(Car).engine.serialNumber).toBe('A-123'); 287 | }); 288 | 289 | it('should support tokenized services from factories', function () { 290 | interface Vehicle { 291 | getColor(): string; 292 | } 293 | 294 | class Bus implements Vehicle { 295 | getColor(): string { 296 | return 'yellow'; 297 | } 298 | } 299 | 300 | class VehicleFactory { 301 | createBus(): Vehicle { 302 | return new Bus(); 303 | } 304 | } 305 | 306 | const VehicleService = new Token(); 307 | 308 | Container.set({ 309 | id: VehicleService, 310 | factory: [VehicleFactory, 'createBus'], 311 | }); 312 | 313 | expect(Container.get(VehicleService).getColor()).toBe('yellow'); 314 | }); 315 | }); 316 | 317 | describe('Container.reset', () => { 318 | it('should call dispose function on removed service', () => { 319 | const destroyFnMock = jest.fn(); 320 | const destroyPropertyFnMock = jest.fn(); 321 | @Service() 322 | class MyServiceA { 323 | dispose() { 324 | destroyFnMock(); 325 | } 326 | } 327 | 328 | @Service() 329 | class MyServiceB { 330 | public dispose = destroyPropertyFnMock; 331 | } 332 | 333 | const instanceAOne = Container.get(MyServiceA); 334 | const instanceBOne = Container.get(MyServiceB); 335 | 336 | Container.reset({ strategy: 'resetValue' }); 337 | 338 | const instanceATwo = Container.get(MyServiceA); 339 | const instanceBTwo = Container.get(MyServiceB); 340 | 341 | expect(destroyFnMock).toBeCalledTimes(1); 342 | expect(destroyPropertyFnMock).toBeCalledTimes(1); 343 | 344 | expect(instanceAOne).toBeInstanceOf(MyServiceA); 345 | expect(instanceATwo).toBeInstanceOf(MyServiceA); 346 | expect(instanceBOne).toBeInstanceOf(MyServiceB); 347 | expect(instanceBTwo).toBeInstanceOf(MyServiceB); 348 | 349 | expect(instanceAOne).not.toBe(instanceATwo); 350 | expect(instanceBOne).not.toBe(instanceBTwo); 351 | }); 352 | 353 | it('should be able to destroy services without destroy function', () => { 354 | @Service() 355 | class MyService {} 356 | 357 | const instanceA = Container.get(MyService); 358 | 359 | Container.reset({ strategy: 'resetValue' }); 360 | 361 | const instanceB = Container.get(MyService); 362 | 363 | expect(instanceA).toBeInstanceOf(MyService); 364 | expect(instanceB).toBeInstanceOf(MyService); 365 | expect(instanceA).not.toBe(instanceB); 366 | }); 367 | }); 368 | 369 | describe('Container.remove', () => { 370 | it('should call dispose function on removed service', () => { 371 | const destroyFnMock = jest.fn(); 372 | const destroyPropertyFnMock = jest.fn(); 373 | @Service() 374 | class MyServiceA { 375 | dispose() { 376 | destroyFnMock(); 377 | } 378 | } 379 | 380 | @Service() 381 | class MyServiceB { 382 | public dispose = destroyPropertyFnMock(); 383 | } 384 | 385 | Container.get(MyServiceA); 386 | Container.get(MyServiceB); 387 | 388 | expect(() => Container.remove(MyServiceA)).not.toThrowError(); 389 | expect(() => Container.remove(MyServiceB)).not.toThrowError(); 390 | 391 | expect(destroyFnMock).toBeCalledTimes(1); 392 | expect(destroyPropertyFnMock).toBeCalledTimes(1); 393 | 394 | expect(() => Container.get(MyServiceA)).toThrowError(ServiceNotFoundError); 395 | expect(() => Container.get(MyServiceB)).toThrowError(ServiceNotFoundError); 396 | }); 397 | 398 | it('should be able to destroy services without destroy function', () => { 399 | @Service() 400 | class MyService {} 401 | 402 | Container.get(MyService); 403 | 404 | expect(() => Container.remove(MyService)).not.toThrowError(); 405 | expect(() => Container.get(MyService)).toThrowError(ServiceNotFoundError); 406 | }); 407 | }); 408 | }); 409 | -------------------------------------------------------------------------------- /test/decorators/Inject.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Container } from '../../src/index'; 3 | import { Service } from '../../src/decorators/service.decorator'; 4 | import { Inject } from '../../src/decorators/inject.decorator'; 5 | import { Token } from '../../src/token.class'; 6 | import { InjectMany } from '../../src/decorators/inject-many.decorator'; 7 | 8 | describe('Inject Decorator', function () { 9 | beforeEach(() => Container.reset({ strategy: 'resetValue' })); 10 | 11 | it('should inject service into class property', function () { 12 | @Service() 13 | class TestService {} 14 | @Service() 15 | class SecondTestService { 16 | @Inject() 17 | testService: TestService; 18 | } 19 | expect(Container.get(SecondTestService).testService).toBeInstanceOf(TestService); 20 | }); 21 | 22 | it('should inject token service properly', function () { 23 | interface Test {} 24 | const ServiceToken = new Token(); 25 | 26 | @Service({ id: ServiceToken }) 27 | class TestService {} 28 | @Service() 29 | class SecondTestService { 30 | @Inject(ServiceToken) 31 | testService: Test; 32 | } 33 | expect(Container.get(SecondTestService).testService).toBeInstanceOf(TestService); 34 | }); 35 | 36 | it('should inject named service into class property', function () { 37 | @Service({ id: 'mega.service' }) 38 | class NamedService {} 39 | @Service() 40 | class SecondTestService { 41 | @Inject('mega.service') 42 | megaService: any; 43 | } 44 | expect(Container.get(SecondTestService).megaService).toBeInstanceOf(NamedService); 45 | }); 46 | 47 | it('should inject service via constructor', function () { 48 | @Service() 49 | class TestService {} 50 | @Service() 51 | class SecondTestService {} 52 | @Service({ id: 'mega.service' }) 53 | class NamedService {} 54 | @Service() 55 | class TestServiceWithParameters { 56 | constructor( 57 | public testClass: TestService, 58 | @Inject(type => SecondTestService) public secondTest: any, 59 | @Inject('mega.service') public megaService: any 60 | ) {} 61 | } 62 | expect(Container.get(TestServiceWithParameters).testClass).toBeInstanceOf(TestService); 63 | expect(Container.get(TestServiceWithParameters).secondTest).toBeInstanceOf(SecondTestService); 64 | expect(Container.get(TestServiceWithParameters).megaService).toBeInstanceOf(NamedService); 65 | }); 66 | 67 | it("should inject service should work with 'many' instances", function () { 68 | interface Car { 69 | name: string; 70 | } 71 | @Service({ id: 'cars', multiple: true }) 72 | class Bmw implements Car { 73 | name = 'BMW'; 74 | } 75 | @Service({ id: 'cars', multiple: true }) 76 | class Mercedes implements Car { 77 | name = 'Mercedes'; 78 | } 79 | @Service({ id: 'cars', multiple: true }) 80 | class Toyota implements Car { 81 | name = 'Toyota'; 82 | } 83 | @Service() 84 | class TestServiceWithParameters { 85 | constructor(@InjectMany('cars') public cars: Car[]) {} 86 | } 87 | 88 | expect(Container.get(TestServiceWithParameters).cars).toHaveLength(3); 89 | 90 | const carNames = Container.get(TestServiceWithParameters).cars.map(car => car.name); 91 | expect(carNames).toContain('BMW'); 92 | expect(carNames).toContain('Mercedes'); 93 | expect(carNames).toContain('Toyota'); 94 | }); 95 | 96 | it('should work with empty decorator on constructor parameter', function () { 97 | @Service() 98 | class InjectedClass {} 99 | 100 | @Service() 101 | class TestService { 102 | constructor(@Inject() public myClass: InjectedClass) {} 103 | } 104 | 105 | const instance = Container.get(TestService); 106 | 107 | expect(instance).toBeInstanceOf(TestService); 108 | expect(instance.myClass).toBeInstanceOf(InjectedClass); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /test/decorators/Service.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Container } from '../../src/index'; 3 | import { Service } from '../../src/decorators/service.decorator'; 4 | import { Token } from '../../src/token.class'; 5 | 6 | describe('Service Decorator', function () { 7 | beforeEach(() => Container.reset({ strategy: 'resetValue' })); 8 | 9 | it('should register class in the container, and its instance should be retrievable', function () { 10 | @Service() 11 | class TestService {} 12 | @Service({ id: 'super.service' }) 13 | class NamedService {} 14 | expect(Container.get(TestService)).toBeInstanceOf(TestService); 15 | expect(Container.get(TestService)).not.toBeInstanceOf(NamedService); 16 | }); 17 | 18 | it('should register class in the container with given name, and its instance should be retrievable', function () { 19 | @Service() 20 | class TestService {} 21 | @Service({ id: 'super.service' }) 22 | class NamedService {} 23 | expect(Container.get('super.service')).toBeInstanceOf(NamedService); 24 | expect(Container.get('super.service')).not.toBeInstanceOf(TestService); 25 | }); 26 | 27 | it('should register class in the container, and its parameter dependencies should be properly initialized', function () { 28 | @Service() 29 | class TestService {} 30 | @Service() 31 | class SecondTestService {} 32 | @Service() 33 | class TestServiceWithParameters { 34 | constructor(public testClass: TestService, public secondTest: SecondTestService) {} 35 | } 36 | expect(Container.get(TestServiceWithParameters)).toBeInstanceOf(TestServiceWithParameters); 37 | expect(Container.get(TestServiceWithParameters).testClass).toBeInstanceOf(TestService); 38 | expect(Container.get(TestServiceWithParameters).secondTest).toBeInstanceOf(SecondTestService); 39 | }); 40 | 41 | it('should support factory functions', function () { 42 | @Service() 43 | class Engine { 44 | constructor(public serialNumber: string) {} 45 | } 46 | 47 | function createCar() { 48 | return new Car('BMW', new Engine('A-123')); 49 | } 50 | 51 | @Service({ factory: createCar }) 52 | class Car { 53 | constructor(public name: string, public engine: Engine) {} 54 | } 55 | 56 | expect(Container.get(Car).name).toBe('BMW'); 57 | expect(Container.get(Car).engine.serialNumber).toBe('A-123'); 58 | }); 59 | 60 | it('should support factory classes', function () { 61 | @Service() 62 | class Engine { 63 | public serialNumber = 'A-123'; 64 | } 65 | 66 | @Service() 67 | class CarFactory { 68 | constructor(public engine: Engine) {} 69 | 70 | createCar() { 71 | return new Car('BMW', this.engine); 72 | } 73 | } 74 | 75 | @Service({ factory: [CarFactory, 'createCar'] }) 76 | class Car { 77 | name: string; 78 | constructor(name: string, public engine: Engine) { 79 | this.name = name; 80 | } 81 | } 82 | 83 | expect(Container.get(Car).name).toBe('BMW'); 84 | expect(Container.get(Car).engine.serialNumber).toBe('A-123'); 85 | }); 86 | 87 | it('should support factory function with arguments', function () { 88 | @Service() 89 | class Engine { 90 | public type = 'V8'; 91 | } 92 | 93 | @Service() 94 | class CarFactory { 95 | createCar(engine: Engine) { 96 | engine.type = 'V6'; 97 | return new Car(engine); 98 | } 99 | } 100 | 101 | @Service({ factory: [CarFactory, 'createCar'] }) 102 | class Car { 103 | constructor(public engine: Engine) {} 104 | } 105 | 106 | expect(Container.get(Car).engine.type).toBe('V6'); 107 | }); 108 | 109 | it('should support transient services', function () { 110 | @Service() 111 | class Car { 112 | public serial = Math.random(); 113 | } 114 | 115 | @Service({ scope: 'transient' }) 116 | class Engine { 117 | public serial = Math.random(); 118 | } 119 | 120 | const car1Serial = Container.get(Car).serial; 121 | const car2Serial = Container.get(Car).serial; 122 | const car3Serial = Container.get(Car).serial; 123 | 124 | const engine1Serial = Container.get(Engine).serial; 125 | const engine2Serial = Container.get(Engine).serial; 126 | const engine3Serial = Container.get(Engine).serial; 127 | 128 | expect(car1Serial).toBe(car2Serial); 129 | expect(car1Serial).toBe(car3Serial); 130 | 131 | expect(engine1Serial).not.toBe(engine2Serial); 132 | expect(engine2Serial).not.toBe(engine3Serial); 133 | expect(engine3Serial).not.toBe(engine1Serial); 134 | }); 135 | 136 | it('should support global services', function () { 137 | @Service() 138 | class Engine { 139 | public name = 'sporty'; 140 | } 141 | 142 | @Service({ scope: 'singleton' }) 143 | class Car { 144 | public name = 'SportCar'; 145 | } 146 | 147 | const globalContainer = Container; 148 | const scopedContainer = Container.of('enigma'); 149 | 150 | expect(globalContainer.get(Car).name).toBe('SportCar'); 151 | expect(scopedContainer.get(Car).name).toBe('SportCar'); 152 | 153 | expect(globalContainer.get(Engine).name).toBe('sporty'); 154 | expect(scopedContainer.get(Engine).name).toBe('sporty'); 155 | 156 | globalContainer.get(Car).name = 'MyCar'; 157 | globalContainer.get(Engine).name = 'regular'; 158 | 159 | expect(globalContainer.get(Car).name).toBe('MyCar'); 160 | expect(scopedContainer.get(Car).name).toBe('MyCar'); 161 | 162 | expect(globalContainer.get(Engine).name).toBe('regular'); 163 | expect(scopedContainer.get(Engine).name).toBe('sporty'); 164 | }); 165 | 166 | it('should support function injection with Token dependencies', function () { 167 | const myToken: Token = new Token('myToken'); 168 | 169 | Container.set({ id: myToken, value: 'test_string' }); 170 | Container.set({ 171 | id: 'my-service-A', 172 | factory: function myServiceFactory(container): string { 173 | return container.get(myToken).toUpperCase(); 174 | }, 175 | }); 176 | 177 | /** 178 | * This is an unusual format and not officially supported, but it should work. 179 | * We can set null as the target, because we have set the ID manually, so it won't be used. 180 | */ 181 | Service({ 182 | id: 'my-service-B', 183 | factory: function myServiceFactory(container): string { 184 | return container.get(myToken).toUpperCase(); 185 | }, 186 | })(null); 187 | 188 | expect(Container.get('my-service-A')).toBe('TEST_STRING'); 189 | expect(Container.get('my-service-B')).toBe('TEST_STRING'); 190 | }); 191 | 192 | it('should support factory functions', function () { 193 | class Engine { 194 | public serialNumber = 'A-123'; 195 | } 196 | 197 | @Service({ 198 | factory: () => new Car(new Engine()), 199 | }) 200 | class Car { 201 | constructor(public engine: Engine) {} 202 | } 203 | 204 | expect(Container.get(Car).engine.serialNumber).toBe('A-123'); 205 | }); 206 | }); 207 | -------------------------------------------------------------------------------- /test/eager-loading-services.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Container } from '../src/index'; 3 | import { Service } from '../src/decorators/service.decorator'; 4 | 5 | describe('Eager loading of services', function () { 6 | beforeEach(() => Container.reset({ strategy: 'resetValue' })); 7 | 8 | describe('Container API', () => { 9 | it('should be able to set eager and lazy service with Container API', () => { 10 | let callOrder = 1; 11 | 12 | class MyService { 13 | public createdAt = callOrder++; 14 | } 15 | 16 | Container.set({ id: 'eager-service', type: MyService, eager: true }); 17 | Container.set({ id: 'lazy-service', type: MyService, eager: false }); 18 | 19 | const timeStampBeforeRequests = callOrder++; 20 | 21 | const eagerService = Container.get('eager-service'); 22 | const lazyService = Container.get('lazy-service'); 23 | 24 | /** Both should resolve to an instance of the service. */ 25 | expect(eagerService).toBeInstanceOf(MyService); 26 | expect(lazyService).toBeInstanceOf(MyService); 27 | 28 | /** Eager service should have a lower creation order number than the reference timestamp. */ 29 | /** Lazy service should have a higher creation order number than the reference timestamp. */ 30 | expect(eagerService.createdAt).toBe(1); 31 | expect(timeStampBeforeRequests).toBe(2); 32 | expect(lazyService.createdAt).toBe(3); 33 | }); 34 | }); 35 | 36 | describe('@Service decorator', () => { 37 | it('should be able to set eager and lazy service with @Service decorator', () => { 38 | let callOrder = 1; 39 | 40 | @Service({ eager: true }) 41 | class MyEagerService { 42 | public createdAt = callOrder++; 43 | } 44 | 45 | @Service({ eager: false }) 46 | class MyLazyService { 47 | public createdAt = callOrder++; 48 | } 49 | 50 | const timeStampBeforeRequests = callOrder++; 51 | 52 | const eagerService = Container.get(MyEagerService); 53 | const lazyService = Container.get(MyLazyService); 54 | 55 | /** Both should resolve to an instance of the service. */ 56 | expect(eagerService).toBeInstanceOf(MyEagerService); 57 | expect(lazyService).toBeInstanceOf(MyLazyService); 58 | 59 | /** Eager service should have a lower creation order number than the reference timestamp. */ 60 | /** Lazy service should have a higher creation order number than the reference timestamp. */ 61 | expect(eagerService.createdAt).toBe(1); 62 | expect(timeStampBeforeRequests).toBe(2); 63 | expect(lazyService.createdAt).toBe(3); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /test/github-issues/102/issue-102.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Container } from '../../../src/index'; 3 | import { Service } from '../../../src/decorators/service.decorator'; 4 | import { Inject } from '../../../src/decorators/inject.decorator'; 5 | 6 | describe('Github Issues', function () { 7 | beforeEach(() => Container.reset({ strategy: 'resetValue' })); 8 | 9 | it('#102 - handle @Inject()-ed values in inherited child classes', () => { 10 | @Service() 11 | class InjectedService {} 12 | 13 | @Service() 14 | class Base { 15 | public constructor( 16 | @Inject('config') 17 | public cfg: any, 18 | public injectedService: InjectedService 19 | ) {} 20 | } 21 | 22 | @Service() 23 | class Child extends Base {} 24 | 25 | const testconfig = { value: 'I AM A CONFIG OBJECT ' }; 26 | Container.set({ id: 'config', value: testconfig }); 27 | 28 | const baseService = Container.get(Base); 29 | const childService = Container.get(Child); 30 | 31 | expect(baseService).toBeInstanceOf(Base); 32 | expect(baseService.injectedService).toBeInstanceOf(InjectedService); 33 | expect(baseService.cfg).toEqual(testconfig); 34 | 35 | expect(childService).toBeInstanceOf(Child); 36 | expect(childService.injectedService).toBeInstanceOf(InjectedService); 37 | expect(childService.cfg).toEqual(testconfig); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/github-issues/112/issue-112.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Container } from '../../../src/index'; 3 | import { Service } from '../../../src/decorators/service.decorator'; 4 | import { Inject } from '../../../src/decorators/inject.decorator'; 5 | 6 | describe('Github Issues', function () { 7 | beforeEach(() => Container.reset({ strategy: 'resetValue' })); 8 | 9 | it('#112 - maximum call stack size error with circular dependencies', () => { 10 | @Service() 11 | class ClassA { 12 | @Inject(() => ClassB) 13 | classB: unknown; 14 | } 15 | 16 | @Service() 17 | class ClassB { 18 | @Inject(() => ClassA) 19 | classA: unknown; 20 | } 21 | 22 | const scopedContainer = Container.of('scoped'); 23 | /** Retrieve the classes from the root container. */ 24 | const rootClassA = Container.get(ClassA); 25 | const rootClassB = Container.get(ClassB); 26 | /** Retrieve the class instances from the cloned container. */ 27 | const scopedClassA = scopedContainer.get(ClassA); 28 | const scopedClassB = scopedContainer.get(ClassB); 29 | 30 | /** Values should be properly resolved in the root. */ 31 | expect(rootClassA).toBeInstanceOf(ClassA); 32 | expect(rootClassA.classB).toBeInstanceOf(ClassB); 33 | expect(rootClassB).toBeInstanceOf(ClassB); 34 | expect(rootClassB.classA).toBeInstanceOf(ClassA); 35 | expect(rootClassA).toStrictEqual(rootClassB.classA); 36 | expect(rootClassB).toStrictEqual(rootClassA.classB); 37 | 38 | /** Values should be properly resolved in the scoped. */ 39 | expect(scopedClassA).toBeInstanceOf(ClassA); 40 | expect(scopedClassA.classB).toBeInstanceOf(ClassB); 41 | expect(scopedClassB).toBeInstanceOf(ClassB); 42 | expect(scopedClassB.classA).toBeInstanceOf(ClassA); 43 | expect(scopedClassA).toStrictEqual(scopedClassB.classA); 44 | expect(scopedClassB).toStrictEqual(scopedClassA.classB); 45 | 46 | /** Two container should not share the exact same instances. */ 47 | expect(rootClassA).not.toStrictEqual(scopedClassA); 48 | expect(rootClassB).not.toStrictEqual(scopedClassB); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/github-issues/151/issue-151.spect.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Container } from '../../../src/index'; 3 | import { Service } from '../../../src/decorators/service.decorator'; 4 | 5 | describe('Github Issues', function () { 6 | beforeEach(() => Container.reset({ strategy: 'resetValue' })); 7 | 8 | it('#151 - should be able to define type when setting service', () => { 9 | /** 10 | * Note: This is more like a behavioral test the use-case showcased below 11 | * should be always possible, even if the API changes. 12 | */ 13 | @Service() 14 | class AuthService { 15 | isAuthorized() { 16 | return 'nope'; 17 | } 18 | } 19 | 20 | @Service() 21 | class DataService { 22 | constructor(public authService: AuthService) {} 23 | } 24 | @Service() 25 | class FakeDataService { 26 | constructor(public authService: AuthService) {} 27 | } 28 | 29 | Container.set({ id: DataService, type: FakeDataService }); 30 | 31 | const instance = Container.get(DataService as any); 32 | 33 | expect(instance).toBeInstanceOf(FakeDataService); 34 | expect(instance.authService).toBeInstanceOf(AuthService); 35 | expect(instance.authService.isAuthorized()).toBe('nope'); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/github-issues/157/issue-157.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Container } from '../../../src/index'; 3 | import { Service } from '../../../src/decorators/service.decorator'; 4 | 5 | describe('Github Issues', function () { 6 | beforeEach(() => Container.reset({ strategy: 'resetValue' })); 7 | 8 | it('#157 - reset should not break transient services', () => { 9 | let creationCounter = 0; 10 | 11 | @Service({ scope: 'transient' }) 12 | class TransientService { 13 | public constructor() { 14 | creationCounter++; 15 | } 16 | } 17 | 18 | Container.get(TransientService); 19 | Container.get(TransientService); 20 | 21 | expect(creationCounter).toBe(2); 22 | 23 | Container.reset({ strategy: 'resetValue' }); 24 | 25 | Container.get(TransientService); 26 | Container.get(TransientService); 27 | 28 | expect(creationCounter).toBe(4); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/github-issues/40/issue-40.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Container } from '../../../src/index'; 3 | import { Service } from '../../../src/decorators/service.decorator'; 4 | import { Inject } from '../../../src/decorators/inject.decorator'; 5 | 6 | describe('github issues > #40 Constructor inject not working', function () { 7 | beforeEach(() => Container.reset({ strategy: 'resetValue' })); 8 | 9 | it('should work properly', function () { 10 | @Service({ id: 'AccessTokenService' }) 11 | class AccessTokenService { 12 | constructor( 13 | @Inject('moment') public moment: any, 14 | @Inject('jsonwebtoken') public jsonwebtoken: any, 15 | @Inject('cfg.auth.jwt') public jwt: any 16 | ) {} 17 | } 18 | 19 | Container.set({ id: 'moment', value: 'A' }); 20 | Container.set({ id: 'jsonwebtoken', value: 'B' }); 21 | Container.set({ id: 'cfg.auth.jwt', value: 'C' }); 22 | const accessTokenService = Container.get('AccessTokenService'); 23 | 24 | expect(accessTokenService.moment).not.toBeUndefined(); 25 | expect(accessTokenService.jsonwebtoken).not.toBeUndefined(); 26 | expect(accessTokenService.jwt).not.toBeUndefined(); 27 | 28 | expect(accessTokenService.moment).toBe('A'); 29 | expect(accessTokenService.jsonwebtoken).toBe('B'); 30 | expect(accessTokenService.jwt).toBe('C'); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/github-issues/41/issue-41.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Container } from '../../../src/index'; 3 | import { Service } from '../../../src/decorators/service.decorator'; 4 | import { Token } from '../../../src/token.class'; 5 | 6 | describe('github issues > #41 Token as service id in combination with factory', function () { 7 | beforeEach(() => Container.reset({ strategy: 'resetValue' })); 8 | 9 | it('should work properly', function () { 10 | interface SomeInterface { 11 | foo(): string; 12 | } 13 | const SomeInterfaceToken = new Token(); 14 | 15 | @Service() 16 | class SomeInterfaceFactory { 17 | create() { 18 | return new SomeImplementation(); 19 | } 20 | } 21 | 22 | @Service({ 23 | id: SomeInterfaceToken, 24 | factory: [SomeInterfaceFactory, 'create'], 25 | }) 26 | class SomeImplementation implements SomeInterface { 27 | foo() { 28 | return 'hello implementation'; 29 | } 30 | } 31 | 32 | Container.set({ id: 'moment', value: 'A' }); 33 | Container.set({ id: 'jsonwebtoken', value: 'B' }); 34 | Container.set({ id: 'cfg.auth.jwt', value: 'C' }); 35 | const someInterfaceImpl = Container.get(SomeInterfaceToken); 36 | expect(someInterfaceImpl.foo()).toBe('hello implementation'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/github-issues/42/issue-42.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Container } from '../../../src/index'; 3 | import { Service } from '../../../src/decorators/service.decorator'; 4 | import { Inject } from '../../../src/decorators/inject.decorator'; 5 | import { CannotInjectValueError } from '../../../src/error/cannot-inject-value.error'; 6 | 7 | describe('github issues > #42 Exception not thrown on missing binding', function () { 8 | beforeEach(() => Container.reset({ strategy: 'resetValue' })); 9 | 10 | it('should work properly', function () { 11 | interface Factory { 12 | create(): void; 13 | } 14 | 15 | expect(() => { 16 | @Service() 17 | class CoffeeMaker { 18 | @Inject() // This is an incorrect usage of TypeDI because Factory is an interface 19 | private factory: Factory; 20 | 21 | make() { 22 | this.factory.create(); 23 | } 24 | } 25 | // We doesn't even need to call `Container.get(CoffeeMaker);`, TypeDI will detect this error, while 26 | // the JS code is parsed and decorators are executed. 27 | }).toThrowError(CannotInjectValueError); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/github-issues/48/issue-48.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Container } from '../../../src/index'; 3 | import { Service } from '../../../src/decorators/service.decorator'; 4 | import { Token } from '../../../src/token.class'; 5 | 6 | describe("github issues > #48 Token service iDs in global container aren't inherited by scoped containers", function () { 7 | beforeEach(() => Container.reset({ strategy: 'resetValue' })); 8 | 9 | it('should work properly', function () { 10 | let poloCounter = 0; 11 | 12 | const FooServiceToken = new Token(); 13 | 14 | @Service({ id: FooServiceToken }) 15 | class FooService implements FooService { 16 | public marco() { 17 | poloCounter++; 18 | } 19 | } 20 | 21 | const scopedContainer = Container.of('myScopredContainer'); 22 | const rootInstance = Container.get(FooServiceToken); 23 | const scopedInstance = scopedContainer.get(FooServiceToken); 24 | 25 | rootInstance.marco(); 26 | scopedInstance.marco(); 27 | 28 | expect(poloCounter).toBe(2); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/github-issues/53/issue-53.spec.ts: -------------------------------------------------------------------------------- 1 | import { Console } from 'console'; 2 | import 'reflect-metadata'; 3 | import { Container } from '../../../src/index'; 4 | import { Service } from '../../../src/decorators/service.decorator'; 5 | import { Token } from '../../../src/token.class'; 6 | 7 | describe('github issues > #53 Token-based services are cached in the Global container even when fetched via a subcontainer', function () { 8 | beforeEach(() => Container.reset({ strategy: 'resetValue' })); 9 | 10 | it('should work properly', function () { 11 | @Service() 12 | class QuestionRepository { 13 | userName: string; 14 | 15 | save() { 16 | return null; 17 | } 18 | } 19 | 20 | const QuestionControllerToken = new Token('QCImpl'); 21 | 22 | @Service({ id: QuestionControllerToken }) 23 | class QuestionControllerImpl { 24 | constructor(protected questionRepository: QuestionRepository) {} 25 | 26 | save(name: string) { 27 | if (name) this.questionRepository.userName = name; 28 | this.questionRepository.save(); 29 | } 30 | } 31 | 32 | const request1 = 'REQUEST_1'; 33 | const controller1 = Container.of(request1).get(QuestionControllerToken); 34 | controller1.save('Timber'); 35 | Container.reset({ strategy: 'resetValue' }); 36 | 37 | const request2 = 'REQUEST_2'; 38 | const controller2 = Container.of(request2).get(QuestionControllerToken); 39 | controller2.save('John'); 40 | Container.reset({ strategy: 'resetValue' }); 41 | 42 | expect(controller1).not.toBe(controller2); 43 | expect(controller1).not.toBe(Container.get(QuestionControllerToken)); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/github-issues/56/issue-56.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Container } from '../../../src/index'; 3 | import { Service } from '../../../src/decorators/service.decorator'; 4 | 5 | describe('github issues > #56 extended class is being overwritten', function () { 6 | beforeEach(() => Container.reset({ strategy: 'resetValue' })); 7 | 8 | it('should work properly', function () { 9 | @Service() 10 | class Rule { 11 | getRule() { 12 | return 'very strict rule'; 13 | } 14 | } 15 | 16 | @Service() 17 | class Whitelist extends Rule { 18 | getWhitelist() { 19 | return ['rule1', 'rule2']; 20 | } 21 | } 22 | 23 | const whitelist = Container.get(Whitelist); 24 | expect(whitelist.getRule).not.toBeUndefined(); 25 | expect(whitelist.getWhitelist).not.toBeUndefined(); 26 | expect(whitelist.getWhitelist()).toEqual(['rule1', 'rule2']); 27 | expect(whitelist.getRule()).toEqual('very strict rule'); 28 | 29 | const rule = Container.get(Rule); 30 | expect(rule.getRule).not.toBeUndefined(); 31 | expect((rule as Whitelist).getWhitelist).toBeUndefined(); 32 | expect(rule.getRule()).toEqual('very strict rule'); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/github-issues/61/issue-61.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Container } from '../../../src/index'; 3 | import { Service } from '../../../src/decorators/service.decorator'; 4 | 5 | describe('Github Issues', function () { 6 | beforeEach(() => Container.reset({ strategy: 'resetValue' })); 7 | 8 | it('#61 - Scoped container creates new instance of service every time', function () { 9 | @Service() 10 | class Car { 11 | public serial = Math.random(); 12 | } 13 | 14 | const fooContainer = Container.of('foo'); 15 | const barContainer = Container.of('bar'); 16 | 17 | const car1Serial = Container.get(Car).serial; 18 | const car2Serial = Container.get(Car).serial; 19 | 20 | const fooCar1Serial = fooContainer.get(Car).serial; 21 | const fooCar2Serial = fooContainer.get(Car).serial; 22 | 23 | const barCar1Serial = barContainer.get(Car).serial; 24 | const barCar2Serial = barContainer.get(Car).serial; 25 | 26 | expect(car1Serial).toEqual(car2Serial); 27 | expect(fooCar1Serial).toEqual(fooCar2Serial); 28 | expect(barCar1Serial).toEqual(barCar2Serial); 29 | 30 | expect(car1Serial).not.toEqual(fooCar1Serial); 31 | expect(car1Serial).not.toEqual(barCar1Serial); 32 | expect(fooCar1Serial).not.toEqual(barCar1Serial); 33 | 34 | expect(Container.of('TEST').get(Car).serial === Container.of('TEST').get(Car).serial).toBe(true); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/github-issues/87/issue-87.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Container } from '../../../src/index'; 3 | import { Service } from '../../../src/decorators/service.decorator'; 4 | import { ServiceNotFoundError } from '../../../src/error/service-not-found.error'; 5 | 6 | describe('Github Issues', function () { 7 | beforeEach(() => Container.reset({ strategy: 'resetValue' })); 8 | 9 | it('#87 - TypeDI should throw error if a dependency is not found', () => { 10 | @Service() 11 | class InjectedClassA {} 12 | 13 | /** This class is not decorated with @Service decorator. */ 14 | class InjectedClassB {} 15 | 16 | @Service() 17 | class MyClass { 18 | constructor(private injectedClassA: InjectedClassA, private injectedClassB: InjectedClassB) {} 19 | } 20 | 21 | expect(() => Container.get(MyClass)).toThrowError(ServiceNotFoundError); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This TS config is added for VS Code, so it understand the test files are 3 | // using the tsconfig.spec.json file instead of the default one. 4 | "extends": "../tsconfig.spec.json", 5 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "target": "es2018", 6 | "lib": ["es2018"], 7 | "outDir": "build/node", 8 | "rootDirs": ["./src"], 9 | "strict": true, 10 | "sourceMap": true, 11 | "inlineSources": true, 12 | "removeComments": false, 13 | "esModuleInterop": true, 14 | "experimentalDecorators": true, 15 | "emitDecoratorMetadata": true, 16 | "forceConsistentCasingInFileNames": true 17 | }, 18 | "exclude": ["build", "node_modules", "sample", "**/*.spec.ts", "test/**"] 19 | } -------------------------------------------------------------------------------- /tsconfig.prod.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.prod.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "outDir": "build/cjs" 6 | }, 7 | } -------------------------------------------------------------------------------- /tsconfig.prod.esm2015.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.prod.json", 3 | "compilerOptions": { 4 | "module": "ES2015", 5 | "outDir": "build/esm2015", 6 | }, 7 | } -------------------------------------------------------------------------------- /tsconfig.prod.esm5.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.prod.json", 3 | "compilerOptions": { 4 | "module": "ES2015", 5 | "target": "ES5", 6 | "outDir": "build/esm5", 7 | }, 8 | } -------------------------------------------------------------------------------- /tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "strict": false, 5 | "declaration": false, 6 | }, 7 | } -------------------------------------------------------------------------------- /tsconfig.prod.types.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.prod.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "emitDeclarationOnly": true, 6 | "outDir": "build/types", 7 | }, 8 | } -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "rootDirs": ["./src", "./test"], 5 | "strict": false, 6 | "strictPropertyInitialization": false, 7 | "sourceMap": false, 8 | "removeComments": true, 9 | "noImplicitAny": false, 10 | }, 11 | "exclude": ["node_modules"] 12 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [ 5 | true, 6 | "check-space" 7 | ], 8 | "indent": [ 9 | true, 10 | "spaces" 11 | ], 12 | "no-duplicate-variable": true, 13 | "no-eval": true, 14 | "no-internal-module": true, 15 | "no-var-keyword": true, 16 | "one-line": [ 17 | true, 18 | "check-open-brace", 19 | "check-whitespace" 20 | ], 21 | "quotemark": [ 22 | true, 23 | "double" 24 | ], 25 | "semicolon": true, 26 | "triple-equals": [ 27 | true, 28 | "allow-null-check" 29 | ], 30 | "typedef-whitespace": [ 31 | true, 32 | { 33 | "call-signature": "nospace", 34 | "index-signature": "nospace", 35 | "parameter": "nospace", 36 | "property-declaration": "nospace", 37 | "variable-declaration": "nospace" 38 | } 39 | ], 40 | "variable-name": [ 41 | true, 42 | "ban-keywords" 43 | ], 44 | "whitespace": [ 45 | true, 46 | "check-branch", 47 | "check-decl", 48 | "check-operator", 49 | "check-separator", 50 | "check-type" 51 | ] 52 | } 53 | } --------------------------------------------------------------------------------