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
1
2
3
4
5 class Dao {
public create(entity: any) {
[...]
}
}
Service.ts
|
|
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)
}
|
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];
}
See injects.ts
Tests
Tests run with mocha.
npm run test
Assertions are wrote with chai, using the expect syntax.
See injects.spec.ts
Execution report is available here
It’s generated with mocha-simple-html-reporter.
npm run test:report
- 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
-
clean : remove build/ directory with rimraf
-
lint : lint source directory with tslint. See tslint.json
-
test:report : run tests with nyc over mocha with mocha-simple-html-reporter. See .nycrc
-
build:typedoc : build typescript documentation with typedoc
-
build:asciidoc : build this documentation with 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 webpack. See webpack.config.js & tsconfig.json
-
build : alias for build:prepare & build:bundle
See package.json
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[]
----