Dependency Injection for TypeScript

Use case

Use a class instance without instantiate it.

The responsibility of providing dependency is delegated to an injector.

This separates the responsibilities of usages and construction.

This also improves readability, testability & maintainability.

Example

Dao.ts
1 2 3 4 5
class Dao { public create(entity: any) { [...] } }
Service.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
import {inject} from 'injects'; (1) import Dao from './Dao'; class Service { private dao: Dao; constructor() { this.dao = inject(Dao); (2) } public create(entity: any) { return this.dao.create(entity); } }
1 imports inject function
2 inject instance of Dao class
1 2
const service: Service = new Service(); service.create({ entityId: 1 });// will call dao.create

You can also write :

1 2 3 4 5 6 7 8 9 10 11
import {inject} from 'injects'; import Dao from './Dao'; class Service { private dao: Dao = inject(Dao); public create(entity: any) { return this.dao.create(entity); } }

Unit test example

Stubbing dependency is easy :

1 2 3 4 5 6
import {Injectable} from 'injects'; import * as sinon from 'sinon'; Injectable('Dao', sinon.stub()); const service: Service = new Service();// will get stub dependency service.create({ entityId: 1 });// will call the stub

Name of dependency’s class is the key used when to register the dependency instance.

Installation

npm install --save injects

Technical documentation

Api

inject

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
/** * Inject unique instance associated with class name or with custom string key. * Create unique class instance if it doesn't exist. * * @param constructorOrKey class constructor or custom string key * @return the unique instance associated with class name or with custom string key */ export function inject(constructorOrKey: Function | string): any { let key: string; if (typeof constructorOrKey === 'string') { key = constructorOrKey; (1) } else { const constructor: Function = constructorOrKey; key = constructor.name; (2) if (!exists(key)) { Injectable(constructor); (3) } } return injectables[key]; (4) }
1 parameter is used as key
2 class name is used as key
3 register new injectable class instance if it doesn’t exist
4 return injectable indexed by key

Injectable

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
/** * Register an injectable. * Can be used as a class decorator or as a function. * * @param constructorOrKey class constructor or custom string key * @param instance custom instance to register (optional with class constructor, mandatory with string key) */ export function Injectable(constructorOrKey: Function | string, instance?: any): void { let key: string, injectable: any; if (typeof constructorOrKey === 'string') { assert.equal(true, !!(instance), 'can\'t define injectable without instance'); key = constructorOrKey; injectable = instance } else { const constructor: Function = constructorOrKey; key = constructor.name; injectable = instance ? instance : new (Object.create(constructor.prototype)).constructor(); } injectables[key] = injectable; }

injectables

An in-memory registry (a map string:any) is used to

1 2 3 4
/** * All injectables. */ export let injectables: { [k: string]: any; } = {};

exists

1 2 3 4 5 6 7 8 9
/** * In order to know if a dependency exists or not. * * @param name dependency name * @returns true if dependency exist, false otherwise */ export function exists(name: string) { return Object.keys(injectables).some(k => k === name); }

resetInjectables

1 2 3 4 5 6
/** * Only for tests : delete all injectables */ export function resetInjectables() { injectables = {}; }

resetInjectable

1 2 3 4 5 6
/** * Only for tests : delete an injectable */ export function resetInjectable(name: string) { delete injectables[name]; }

Lint

Code is lint with tslint. See tslint.json

npm run lint

Packaging

Bundle is generated with webpack. See webpack.config.js

npm run build:bundle

Documentation

This technical documentation is generated with asciidoctor.

npm run build:doc:living

Typescript documentation is available here

It’s generated with typedoc.

npm run build:doc:typedoc

Tests

Tests run with mocha.

npm run test

Assertions are wrote with chai, using the expect syntax.

Execution report is available here

It’s generated with mocha-simple-html-reporter.

npm run test:report
Mocha
  • passes: 12
  • failures: 0
  • duration: 26ms
  • Dependency Injection

    • should register dependency3ms

      __awaiter(this, void 0, void 0, function* () {
              // Given
              class Sample {
              }
              assume(injects_1.exists('Sample')).to.be.false;
              // When
              injects_1.Injectable(Sample);
              // Then
              chai_1.expect(injects_1.exists('Sample')).to.be.true;
          })
    • should inject dependency1ms

      __awaiter(this, void 0, void 0, function* () {
              // Given
              class Sample {
              }
              assume(injects_1.exists('Sample')).to.be.false;
              injects_1.Injectable(Sample);
              assume(injects_1.exists('Sample')).to.be.true;
              // When
              const sample = injects_1.inject(Sample);
              // Then
              chai_1.expect(sample).to.be.an.instanceOf(Sample);
          })
    • should inject class dependency0ms

      __awaiter(this, void 0, void 0, function* () {
              // Given
              class Sample {
              }
              assume(injects_1.exists('Sample')).to.be.false;
              // When
              const sample = injects_1.inject(Sample);
              // Then
              chai_1.expect(sample).to.not.be.null;
              true;
          })
    • should inject custom object instance0ms

      __awaiter(this, void 0, void 0, function* () {
              // Given
              assume(injects_1.exists('sample')).to.be.false;
              class Sample {
                  constructor(value) {
                      this.value = value;
                  }
              }
              const sampleValue = 'testSampleValue';
              injects_1.Injectable('sample', new Sample(sampleValue));
              assume(injects_1.exists('sample')).to.be.true;
              // When
              const sample = injects_1.inject('sample');
              // Then
              chai_1.expect(sample.value).to.eql(sampleValue);
          })
    • should inject custom class instance0ms

      __awaiter(this, void 0, void 0, function* () {
              // Given
              assume(injects_1.exists('Sample')).to.be.false;
              class Sample {
                  constructor(value) {
                      this.value = value;
                  }
              }
              const sampleValue = 'testSampleValue';
              injects_1.Injectable(Sample, new Sample(sampleValue));
              assume(injects_1.exists('Sample')).to.be.true;
              // When
              const sample = injects_1.inject(Sample);
              // Then
              chai_1.expect(sample.value).to.eql(sampleValue);
          })
    • should throw assert exception when creating Injectable with custom string key but without instance14ms

      __awaiter(this, void 0, void 0, function* () {
              // Given
              assume(injects_1.exists('sample')).to.be.false;
              let assertionError = false;
              try {
                  // When
                  injects_1.Injectable('sample');
              }
              catch (e) {
                  assertionError = true;
              }
              // Then
              chai_1.expect(assertionError).to.be.true;
          })
    • should inject string1ms

      __awaiter(this, void 0, void 0, function* () {
              // Given
              assume(injects_1.exists('sample')).to.be.false;
              const sampleValue = 'testSample';
              injects_1.Injectable('sample', sampleValue);
              assume(injects_1.exists('sample')).to.be.true;
              // When
              const sample = injects_1.inject('sample');
              // Then
              chai_1.expect(sample).to.eql(sampleValue);
              chai_1.expect(sample).to.be.a('string');
          })
    • should inject number0ms

      __awaiter(this, void 0, void 0, function* () {
              // Given
              assume(injects_1.exists('sample')).to.be.false;
              const sampleValue = 1;
              injects_1.Injectable('sample', sampleValue);
              assume(injects_1.exists('sample')).to.be.true;
              // When
              const sample = injects_1.inject('sample');
              // Then
              chai_1.expect(sample).to.eql(sampleValue);
              chai_1.expect(sample).to.be.a('number');
          })
    • should register @Injectable1ms

      __awaiter(this, void 0, void 0, function* () {
              // Given
              assume(injects_1.exists('Sample')).to.be.false;
              // When
              let Sample = class Sample {
              };
              Sample = __decorate([
                  injects_1.Injectable
              ], Sample);
              // Then
              chai_1.expect(injects_1.exists('Sample')).to.be.true;
          })
    • should inject in cascade0ms

      __awaiter(this, void 0, void 0, function* () {
              // Given
              let One = class One {
              };
              One = __decorate([
                  injects_1.Injectable
              ], One);
              let Two = class Two {
                  constructor() { this.one = injects_1.inject(One); }
              };
              Two = __decorate([
                  injects_1.Injectable
              ], Two);
              let Three = class Three {
                  constructor() { this.two = injects_1.inject(Two); }
              };
              Three = __decorate([
                  injects_1.Injectable
              ], Three);
              // When
              const three = injects_1.inject(Three);
              // Then
              chai_1.expect(three).to.be.an.instanceOf(Three);
              chai_1.expect(three.two).to.be.an.instanceOf(Two);
              chai_1.expect(three.two.one).to.be.an.instanceOf(One);
          })
    • should reset injectables0ms

      __awaiter(this, void 0, void 0, function* () {
              // Given
              assume(injects_1.exists('Sample')).to.be.false;
              assume(injects_1.exists('Sample')).to.be.false;
              class Sample {
              }
              class Sample2 {
              }
              injects_1.Injectable(Sample);
              injects_1.Injectable(Sample2);
              chai_1.expect(injects_1.exists('Sample')).to.be.true;
              chai_1.expect(injects_1.exists('Sample2')).to.be.true;
              // When
              injects_1.resetInjectables();
              // Then
              chai_1.expect(injects_1.exists('Sample')).to.be.false;
              chai_1.expect(injects_1.exists('Sample2')).to.be.false;
          })
    • should reset injectable1ms

      __awaiter(this, void 0, void 0, function* () {
              // Given
              assume(injects_1.exists('Sample')).to.be.false;
              assume(injects_1.exists('Sample')).to.be.false;
              class Sample {
              }
              class Sample2 {
              }
              injects_1.Injectable(Sample);
              injects_1.Injectable(Sample2);
              chai_1.expect(injects_1.exists('Sample')).to.be.true;
              chai_1.expect(injects_1.exists('Sample2')).to.be.true;
              // When
              injects_1.resetInjectable('Sample');
              // Then
              chai_1.expect(injects_1.exists('Sample')).to.be.false;
              chai_1.expect(injects_1.exists('Sample2')).to.be.true;
          })

Coverage report is available here

It’s generated with nyc.

npm run test:report

See .nycrc config file.

Unit tests

should register dependency
1 2 3 4 5 6 7 8 9 10 11
it('should register dependency', async () => { // Given class Sample {} assume(exists('Sample')).to.be.false; // When Injectable(Sample); // Then expect(exists('Sample')).to.be.true; });
should inject dependency
1 2 3 4 5 6 7 8 9 10 11 12 13
it('should inject dependency', async () => { // Given class Sample {} assume(exists('Sample')).to.be.false; Injectable(Sample); assume(exists('Sample')).to.be.true; // When const sample = inject(Sample); // Then expect(sample).to.be.an.instanceOf(Sample); });
should inject class dependency
1 2 3 4 5 6 7 8 9 10 11
it('should inject class dependency', async () => { // Given class Sample {} assume(exists('Sample')).to.be.false; // When const sample = inject(Sample); // Then expect(sample).to.not.be.null;true; });
should inject custom object instance
1 2 3 4 5 6 7 8 9 10 11 12 13 14
it('should inject custom object instance', async () => { // Given assume(exists('sample')).to.be.false; class Sample { constructor(public value: string) {} } const sampleValue = 'testSampleValue'; Injectable('sample', new Sample(sampleValue)); assume(exists('sample')).to.be.true; // When const sample:Sample = inject('sample'); // Then expect(sample.value).to.eql(sampleValue); });
should inject custom class instance
1 2 3 4 5 6 7 8 9 10 11 12 13 14
it('should inject custom class instance', async () => { // Given assume(exists('Sample')).to.be.false; class Sample { constructor(public value: string) {} } const sampleValue = 'testSampleValue'; Injectable(Sample, new Sample(sampleValue)); assume(exists('Sample')).to.be.true; // When const sample:Sample = inject(Sample); // Then expect(sample.value).to.eql(sampleValue); });
should throw assert exception when creating injectable with custom string key but without instance
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
it('should throw assert exception when creating Injectable with custom string key but without instance', async () => { // Given assume(exists('sample')).to.be.false; let assertionError = false; try { // When Injectable('sample'); } catch (e) { assertionError = true; } // Then expect(assertionError).to.be.true; });
should inject string
1 2 3 4 5 6 7 8 9 10 11 12 13 14
it('should inject string', async () => { // Given assume(exists('sample')).to.be.false; const sampleValue: string = 'testSample'; Injectable('sample', sampleValue); assume(exists('sample')).to.be.true; // When const sample: string = inject('sample'); // Then expect(sample).to.eql(sampleValue); expect(sample).to.be.a('string'); });
should inject number
1 2 3 4 5 6 7 8 9 10 11 12 13 14
it('should inject number', async () => { // Given assume(exists('sample')).to.be.false; const sampleValue: number = 1; Injectable('sample', sampleValue); assume(exists('sample')).to.be.true; // When const sample: number = inject('sample'); // Then expect(sample).to.eql(sampleValue); expect(sample).to.be.a('number'); });
should register @Injectable
1 2 3 4 5 6 7 8 9 10
it('should register @Injectable', async () => { // Given assume(exists('Sample')).to.be.false; // When @Injectable class Sample {} // Then expect(exists('Sample')).to.be.true; });
should inject in cascade
1 2 3 4 5 6 7 8 9 10 11 12 13 14
it('should inject in cascade', async () => { // Given @Injectable class One {} @Injectable class Two { public one: One; constructor() { this.one = inject(One); } } @Injectable class Three { public two: Two; constructor() { this.two = inject(Two); } } // When const three = inject(Three); // Then expect(three).to.be.an.instanceOf(Three); expect(three.two).to.be.an.instanceOf(Two); expect(three.two.one).to.be.an.instanceOf(One); });
should reset injectables
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
it('should reset injectables', async () => { // Given assume(exists('Sample')).to.be.false; assume(exists('Sample')).to.be.false; class Sample {} class Sample2 {} Injectable(Sample); Injectable(Sample2); expect(exists('Sample')).to.be.true; expect(exists('Sample2')).to.be.true; // When resetInjectables(); // Then expect(exists('Sample')).to.be.false; expect(exists('Sample2')).to.be.false; });
should reset injectable
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
it('should reset injectable', async () => { // Given assume(exists('Sample')).to.be.false; assume(exists('Sample')).to.be.false; class Sample {} class Sample2 {} Injectable(Sample); Injectable(Sample2); expect(exists('Sample')).to.be.true; expect(exists('Sample2')).to.be.true; // When resetInjectable('Sample'); // Then expect(exists('Sample')).to.be.false; expect(exists('Sample2')).to.be.true; });

Development

Prerequisites

Npm scripts

Files

package.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
{ "name": "injects", "version": "1.0.0", "description": "Dependency Injection for Typescript", "author": "mickgrz", "main": "./build/injects.js", "typings": "./build/injects.d.ts", "scripts": { "clean": "rimraf build/* && rimraf docs/* && mkdir -p docs/test", "lint": "tslint injects.ts injects.spec.ts", "test": "./node_modules/.bin/mocha --reporter spec --compilers ts:ts-node/register injects.spec.ts", "test:report": "./node_modules/.bin/nyc ./node_modules/.bin/mocha --reporter mocha-simple-html-reporter --reporter-options output=docs/test/index.html --compilers ts:ts-node/register --require source-map-support/register --full-trace --bail injects.spec.ts", "build:typedoc": "./node_modules/.bin/typedoc --theme minimal --readme none --name 'injects' --out docs/typedoc --mode file --exclude injects.spec.ts", "build:asciidoc": "asciidoctor -a linkcss -r asciidoctor-diagram documentation -o docs/index.html", "build:doc": "npm run build:typedoc && npm run build:asciidoc", "build:prepare": "npm run clean && npm run lint && npm run test:report && npm run build:doc", "build:bundle": "./node_modules/.bin/webpack --debug", "build": "npm run build:prepare && npm run build:bundle", "publish": "npm run build && npm publish" }, "devDependencies": { "@types/chai": "^3.4.35", "@types/mocha": "^2.2.39", "chai": "^3.5.0", "mocha": "^3.2.0", "mocha-simple-html-reporter": "^1.0.1", "nyc": "^10.1.2-candidate.0", "rimraf": "^2.6.0", "source-map-loader": "^0.2.1", "source-map-support": "^0.4.11", "ts-loader": "^2.0.1", "ts-node": "^2.1.0", "tslint": "^4.4.2", "tslint-loader": "^3.4.3", "typedoc": "^0.5.7", "typescript": "^2.2.1", "webpack": "^2.2.1" }, "repository": { "type": "git", "url": "git+https://github.com/mickgrz/injects.git" }, "bugs": { "url": "https://github.com/mickgrz/injects/issues" }, "homepage": "https://github.com/mickgrz/injects#readme", "keywords": [ "injects", "dependency", "injection", "ioc", "clean", "code", "cleancode", "typescript", "tslint", "mocha", "chai", "nyc", "typedoc", "webpack", "asciidoctor", "asciidoctor-diagram" ] }
injects.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
// tag::import[] import * as assert from 'assert'; // end::import[] // tag::injectables[] /** * All injectables. */ export let injectables: { [k: string]: any; } = {}; // end::injectables[] // tag::Injectable[] /** * Register an injectable. * Can be used as a class decorator or as a function. * * @param constructorOrKey class constructor or custom string key * @param instance custom instance to register (optional with class constructor, mandatory with string key) */ export function Injectable(constructorOrKey: Function | string, instance?: any): void { let key: string, injectable: any; if (typeof constructorOrKey === 'string') { assert.equal(true, !!(instance), 'can\'t define injectable without instance'); key = constructorOrKey; injectable = instance } else { const constructor: Function = constructorOrKey; key = constructor.name; injectable = instance ? instance : new (Object.create(constructor.prototype)).constructor(); } injectables[key] = injectable; } // end::Injectable[] // tag::inject[] /** * Inject unique instance associated with class name or with custom string key. * Create unique class instance if it doesn't exist. * * @param constructorOrKey class constructor or custom string key * @return the unique instance associated with class name or with custom string key */ export function inject(constructorOrKey: Function | string): any { let key: string; if (typeof constructorOrKey === 'string') { key = constructorOrKey; } else {(1) const constructor: Function = constructorOrKey; key = constructor.name; if (!exists(key)) {(2) Injectable(constructor); }(3) } return injectables[key]; }(4) // end::inject[] // tag::exists[] /** * In order to know if a dependency exists or not. * * @param name dependency name * @returns true if dependency exist, false otherwise */ export function exists(name: string) { return Object.keys(injectables).some(k => k === name); } // end::exists[] // Only for tests // tag::resetInjectables[] /** * Only for tests : delete all injectables */ export function resetInjectables() { injectables = {}; } // end::resetInjectables[] // tag::resetInjectable[] /** * Only for tests : delete an injectable */ export function resetInjectable(name: string) { delete injectables[name]; } // end::resetInjectable[]
tslint.json
1 2 3 4 5 6 7
{ "rules": { "indent": [true, "spaces"], "no-trailing-whitespace": true, "quotemark": [true, "single"] } }
injects.spec.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
import {expect} from 'chai'; import {exists, inject, Injectable, resetInjectables, resetInjectable} from './injects'; const assume = expect; describe('Dependency Injection', () => { afterEach(resetInjectables); // tag::shouldRegisterDependency[] it('should register dependency', async () => { // Given class Sample {} assume(exists('Sample')).to.be.false; // When Injectable(Sample); // Then expect(exists('Sample')).to.be.true; }); // end::shouldRegisterDependency[] // tag::shouldInjectDependency[] it('should inject dependency', async () => { // Given class Sample {} assume(exists('Sample')).to.be.false; Injectable(Sample); assume(exists('Sample')).to.be.true; // When const sample = inject(Sample); // Then expect(sample).to.be.an.instanceOf(Sample); }); // end::shouldInjectDependency[] // tag::shouldInjectClassDependency[] it('should inject class dependency', async () => { // Given class Sample {} assume(exists('Sample')).to.be.false; // When const sample = inject(Sample); // Then expect(sample).to.not.be.null;true; }); // end::shouldInjectClassDependency[] // tag::shouldInjectCustomObjectInstance[] it('should inject custom object instance', async () => { // Given assume(exists('sample')).to.be.false; class Sample { constructor(public value: string) {} } const sampleValue = 'testSampleValue'; Injectable('sample', new Sample(sampleValue)); assume(exists('sample')).to.be.true; // When const sample:Sample = inject('sample'); // Then expect(sample.value).to.eql(sampleValue); }); // end::shouldInjectCustomObjectInstance[] // tag::shouldInjectCustomClassInstance[] it('should inject custom class instance', async () => { // Given assume(exists('Sample')).to.be.false; class Sample { constructor(public value: string) {} } const sampleValue = 'testSampleValue'; Injectable(Sample, new Sample(sampleValue)); assume(exists('Sample')).to.be.true; // When const sample:Sample = inject(Sample); // Then expect(sample.value).to.eql(sampleValue); }); // end::shouldInjectCustomClassInstance[] // tag::shouldThrowAssertExceptionWhenCreatingInjectableWithCustomStringKeyButWithoutInstance[] it('should throw assert exception when creating Injectable with custom string key but without instance', async () => { // Given assume(exists('sample')).to.be.false; let assertionError = false; try { // When Injectable('sample'); } catch (e) { assertionError = true; } // Then expect(assertionError).to.be.true; }); // end::shouldThrowAssertExceptionWhenCreatingInjectableWithCustomStringKeyButWithoutInstance[] // tag::shouldInjectString[] it('should inject string', async () => { // Given assume(exists('sample')).to.be.false; const sampleValue: string = 'testSample'; Injectable('sample', sampleValue); assume(exists('sample')).to.be.true; // When const sample: string = inject('sample'); // Then expect(sample).to.eql(sampleValue); expect(sample).to.be.a('string'); }); // end::shouldInjectString[] // tag::shouldInjectNumber[] it('should inject number', async () => { // Given assume(exists('sample')).to.be.false; const sampleValue: number = 1; Injectable('sample', sampleValue); assume(exists('sample')).to.be.true; // When const sample: number = inject('sample'); // Then expect(sample).to.eql(sampleValue); expect(sample).to.be.a('number'); }); // end::shouldInjectNumber[] // tag::shouldRegister@Injectable[] it('should register @Injectable', async () => { // Given assume(exists('Sample')).to.be.false; // When @Injectable class Sample {} // Then expect(exists('Sample')).to.be.true; }); // end::shouldRegister@Injectable[] // tag::shouldInjectInCascade[] it('should inject in cascade', async () => { // Given @Injectable class One {} @Injectable class Two { public one: One; constructor() { this.one = inject(One); } } @Injectable class Three { public two: Two; constructor() { this.two = inject(Two); } } // When const three = inject(Three); // Then expect(three).to.be.an.instanceOf(Three); expect(three.two).to.be.an.instanceOf(Two); expect(three.two.one).to.be.an.instanceOf(One); }); // end::shouldInjectInCascade[] // tag::shouldResetInjectables[] it('should reset injectables', async () => { // Given assume(exists('Sample')).to.be.false; assume(exists('Sample')).to.be.false; class Sample {} class Sample2 {} Injectable(Sample); Injectable(Sample2); expect(exists('Sample')).to.be.true; expect(exists('Sample2')).to.be.true; // When resetInjectables(); // Then expect(exists('Sample')).to.be.false; expect(exists('Sample2')).to.be.false; }); // end::shouldResetInjectables[] // tag::shouldResetInjectable[] it('should reset injectable', async () => { // Given assume(exists('Sample')).to.be.false; assume(exists('Sample')).to.be.false; class Sample {} class Sample2 {} Injectable(Sample); Injectable(Sample2); expect(exists('Sample')).to.be.true; expect(exists('Sample2')).to.be.true; // When resetInjectable('Sample'); // Then expect(exists('Sample')).to.be.false; expect(exists('Sample2')).to.be.true; }); // end::shouldResetInjectable[] });
.nycrc
1 2 3 4 5 6 7 8 9 10 11 12 13 14
{ "include": "injects.ts", "extension": ".ts", "require": "ts-node/register", "reporter": [ "json", "text-summary", "html" ], "sourceMap": true, "instrument": true, "report-dir": "./docs/test/coverage", "all": true }
webpack.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
'use strict'; module.exports = { entry: './injects.ts', target: 'node', output: { path: __dirname, filename: `build/injects.js`, libraryTarget: 'commonjs' }, resolve: { extensions: ['.js', '.ts'] }, devtool: 'source-map', module: { loaders: [ { test: /\.ts$/, enforce: 'pre', loader: 'tslint-loader', options: { emitErrors: true, failOnHint: true } }, { test: /\.ts$/, enforce: 'pre', loader: "source-map-loader" }, { test: /\.ts$/, loader: 'ts-loader', exclude: /node_modules/, query: { presets: ["es2015"] } } ] } };
tsconfig.json
1 2 3 4 5 6 7 8 9 10 11 12 13
{ "compilerOptions": { "lib": ["es2015", "dom"], "target": "es6", "module": "commonjs", "noImplicitAny": true, "experimentalDecorators": true, "declaration": true, "removeComments": false, "outDir": "./build" }, "files": ["injects.ts"] }
.editorconfig
1 2 3 4 5 6 7 8 9 10 11
; indicate this is the root of the project root = true [*] indent_style = space end_of_line = lf indent_size = 2 charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true
.gitignore
1 2 3 4
*.swp node_modules .nyc_output npm-debug.log
readme.md
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
# Injects Dependency injection for TypeScript : https://mickgrz.github.io/injects/ ## Use case : Use a class instance without instantiate it. The responsibility of providing dependency is delegated to an injector. This separates the responsibilities of usages and construction. This also improves readability, testability & maintainability. ### Example : ```js class Dao { public create(entity: any) { [...] } } ``` ```js import {inject} from 'injects'; import Dao from './Dao'; class Service { private dao: Dao; constructor() { this.dao = inject(Dao); } public create(entity: any) { return this.dao.create(entity); } } ``` ```js const service: Service = new Service(); service.create({ entityId: 1 });// will call dao.create ``` You can also write : ```js import {inject} from 'injects'; import Dao from './Dao'; class Service { private dao: Dao = inject(Dao); public create(entity: any) { return this.dao.create(entity); } } ``` ### Unit test example : Stubbing dependency is easy : ```js import {Injectable} from 'injects'; import * as sinon from 'sinon'; Injectable('Dao', sinon.stub()); const service: Service = new Service();// will get stub dependency service.create({ entityId: 1 });// will call the stub ``` Name of dependency's class is the key used to register the dependency instance. ## Installation : ``` npm install --save injects ```
index.adoc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429
= injects :toc: left :toc-title: Dependency Injection for TypeScript :toclevels: 4 :source-highlighter: pygments :icons: font :build-doc-dir: docs :linkattrs: v1.0.0, April 3, 2017 // All links of documentation :link-webpack: https://github.com/webpack/webpack[webpack, window="_blank"] :link-asciidoctor: https://github.com/asciidoctor[asciidoctor, window="_blank"] :link-typedoc: https://github.com/TypeStrong/typedoc[typedoc, window="_blank"] :link-mocha: https://github.com/mochajs/mocha[mocha, window="_blank"] :link-chai: https://github.com/chaijs/chai[chai, window="_blank"] :link-mocha-simple-html-reporter: https://github.com/blond/mocha-simple-html-reporter[mocha-simple-html-reporter, window="_blank"] :link-nyc: https://github.com/istanbuljs/nyc[nyc, window="_blank"] :link-rimraf: https://github.com/isaacs/rimraf[rimraf, window="_blank"] :link-tslint: https://github.com/palantir/tslint[tslint, window="_blank"] https://mickgrz.github.io/injects/ [sidebar] .Dependency Injection for TypeScript -- -- == Use case Use a class instance without instantiate it. The responsibility of providing dependency is delegated to an injector. This separates the responsibilities of usages and construction. This also improves readability, testability & maintainability. === Example [source%nowrap,ts,linenums] .Dao.ts ---- class Dao { public create(entity: any) { [...] } } ---- [cols="2"] |=== a| [source%nowrap,ts,linenums] .Service.ts ---- import {inject} from 'injects'; (1) import Dao from './Dao'; class Service { private dao: Dao; constructor() { this.dao = inject(Dao); (2) } public create(entity: any) { return this.dao.create(entity); } } ---- a| [TIP] -- <1> imports inject function <2> inject instance of Dao class -- |=== [source%nowrap,ts,linenums] ---- const service: Service = new Service(); service.create({ entityId: 1 });// will call dao.create ---- You can also write : [source%nowrap,ts,linenums] ---- import {inject} from 'injects'; import Dao from './Dao'; class Service { private dao: Dao = inject(Dao); public create(entity: any) { return this.dao.create(entity); } } ---- === Unit test example Stubbing dependency is easy : [source%nowrap,ts,linenums] ---- import {Injectable} from 'injects'; import * as sinon from 'sinon'; Injectable('Dao', sinon.stub()); const service: Service = new Service();// will get stub dependency service.create({ entityId: 1 });// will call the stub ---- Name of dependency's class is the key used when to register the dependency instance. == Installation ``` npm install --save injects ``` == Technical documentation === Api ==== inject [source%nowrap,ts,linenums] ---- include::injects.ts[tags=inject] ---- [TIP] -- <1> parameter is used as key <2> class name is used as key <3> register new injectable class instance if it doesn't exist <4> return injectable indexed by key -- ==== Injectable [source%nowrap,ts,linenums] ---- include::injects.ts[tags=Injectable] ---- ==== injectables An in-memory registry (a map string:any) is used to [source%nowrap,ts,linenums] ---- include::injects.ts[tags=injectables] ---- ==== exists [source%nowrap,ts,linenums] ---- include::injects.ts[tags=exists] ---- ==== resetInjectables [source%nowrap,ts,linenums] ---- include::injects.ts[tags=resetInjectables] ---- ==== resetInjectable [source%nowrap,ts,linenums] ---- include::injects.ts[tags=resetInjectable] ---- See <<injects.ts>> === Lint Code is lint with {link-tslint}. See <<tslint.json>> npm run lint === Packaging Bundle is generated with {link-webpack}. See <<webpack.config.js>> npm run build:bundle === Documentation This technical documentation is generated with {link-asciidoctor}. npm run build:doc:living ==== Typescript documentation is available link:typedoc/index.html[here, window="_blank"] It's generated with {link-typedoc}. npm run build:doc:typedoc === Tests Tests run with {link-mocha}. npm run test Assertions are wrote with {link-chai}, using the *expect syntax*. See <<injects.spec.ts>> ==== Execution report is available link:test/index.html[here, window="_blank"] It's generated with {link-mocha-simple-html-reporter}. npm run test:report ++++ include::{build-doc-dir}/test/index.html[] ++++ ==== Coverage report is available link:test/coverage/index.html[here, window="_blank"] It's generated with {link-nyc}. npm run test:report See <<nycrc>> config file. ==== Unit tests ===== should register dependency [source%nowrap,ts,linenums] ---- include::injects.spec.ts[tags=shouldRegisterDependency] ---- ===== should inject dependency [source%nowrap,ts,linenums] ---- include::injects.spec.ts[tags=shouldInjectDependency] ---- ===== should inject class dependency [source%nowrap,ts,linenums] ---- include::injects.spec.ts[tags=shouldInjectClassDependency] ---- ===== should inject custom object instance [source%nowrap,ts,linenums] ---- include::injects.spec.ts[tags=shouldInjectCustomObjectInstance] ---- ===== should inject custom class instance [source%nowrap,ts,linenums] ---- include::injects.spec.ts[tags=shouldInjectCustomClassInstance] ---- ===== should throw assert exception when creating injectable with custom string key but without instance [source%nowrap,ts,linenums] ---- include::injects.spec.ts[tags=shouldThrowAssertExceptionWhenCreatingInjectableWithCustomStringKeyButWithoutInstance] ---- ===== should inject string [source%nowrap,ts,linenums] ---- include::injects.spec.ts[tags=shouldInjectString] ---- ===== should inject number [source%nowrap,ts,linenums] ---- include::injects.spec.ts[tags=shouldInjectNumber] ---- ===== should register @Injectable [source%nowrap,ts,linenums] ---- include::injects.spec.ts[tags=shouldRegister@Injectable] ---- ===== should inject in cascade [source%nowrap,ts,linenums] ---- include::injects.spec.ts[tags=shouldInjectInCascade] ---- ===== should reset injectables [source%nowrap,ts,linenums] ---- include::injects.spec.ts[tags=shouldResetInjectables] ---- ===== should reset injectable [source%nowrap,ts,linenums] ---- include::injects.spec.ts[tags=shouldResetInjectable] ---- === Development ==== Prerequisites * {link-asciidoctor} ==== Npm scripts * **clean** : remove build/ directory with {link-rimraf} * **lint** : lint source directory with {link-tslint}. See <<tslint.json>> * **test** : run tests with {link-mocha} & {link-chai} * **test:report** : run tests with {link-nyc} over {link-mocha} with {link-mocha-simple-html-reporter}. See <<nycrc>> * **build:typedoc** : build typescript documentation with {link-typedoc} * **build:asciidoc** : build this documentation with {link-asciidoctor}. See <<index.adoc>> * **build:doc** : alias for build:doc:* * **build:prepare** : alias for clean, lint, test:report, build:doc * **build:bundle** : create bundle with {link-webpack}. See <<webpack.config.js>> & <<tsconfig.json>> * **build** : alias for build:prepare & build:bundle See <<package.json>> ==== Files [[package.json]] ===== package.json [source%nowrap,json,linenums] ---- include::package.json[] ---- [[injects.ts]] ===== injects.ts [source%nowrap,ts,linenums] ---- include::injects.ts[] ---- [[tslint.json]] ===== tslint.json [source%nowrap,json,linenums] ---- include::tslint.json[] ---- [[injects.spec.ts]] ===== injects.spec.ts [source%nowrap,ts,linenums] ---- include::injects.spec.ts[] ---- [[nycrc]] ===== .nycrc [source%nowrap,json,linenums] ---- include::.nycrc[] ---- [[webpack.config.js]] ===== webpack.config.js [source%nowrap,js,linenums] ---- include::webpack.config.js[] ---- [[tsconfig.json]] ===== tsconfig.json [source%nowrap,json,linenums] ---- include::tsconfig.json[] ---- [[editorconfig]] ===== .editorconfig [source%nowrap,editorconfig,linenums] ---- include::.editorconfig[] ---- [[gitignore]] ===== .gitignore [source%nowrap,gitignore,linenums] ---- include::.gitignore[] ---- [[readme.md]] ===== readme.md [source%nowrap,markdown,linenums] ---- include::readme.md[] ----