Originally posted on: samueleresca.net
The following article is about inversion of control(IoC) and d*ependency injection(DI)* in Typescript. The main aims of that techniques is to provide loose coupling between modules and classes. IoC and DI are part of the SOLID topics, SOLID principles using Typescript can gives you more notions about SOLID combined with the Typescript world. I have already written about Dependency injection: Dependency Injection overview. The article contains some general informations about Dependency injection base concepts. I also suggest another cool explanation of Dependency injection: How to explain dependency injection to a 5-year-old? .
Reasons for use Inversion of control and Dependency Injection
Here are some reasons for use Inversion of control and Dependency injection:
- Decoupling:Â dependency injection makes your modules less coupled resulting in a more maintainable codebase;
- Easier unit testing:Â instead of using hardcoded dependencies you can pass them into the module you would like to use;
- Faster development:Â with dependency injection, after the interfaces are defined it is easy to work without any merge conflicts;
Inversify JS
InversifyJS is a lightweight  inversion of control (IoC) container for TypeScript and JavaScript apps. A IoC container uses a class constructor to identify and inject its dependencies. InversifyJS has a friendly API and encourage the usage of the best OOP and IoC practices.
Philosophy
InversifyJS has been developed with 4 main goals:
- Allow JavaScript developers to write code that adheres to the SOLID principles.
- Facilitate and encourage the adherence to the best OOP and IoC practices.
- Add as little runtime overhead as possible.
- Provide a state of the art development experience.
Inversify JS in action
The following example will use Typescript combined with InversifyJS to implement some basic logics. All classes will implement interfaces. An unit tests suite, written over Jest, will cover all the code base. It will use Dependency injection to mock behaviours and functions behind the classes. Firstly, let's give you an overview of the project structure: Â The
Track
class defines the Domain model of our system. The IMusicRepository
defines an independent interface which aim is to expose repository common functions, such as CRUD operations. The VinylCatalog
class implements the IMusicRepository
and query a fake db which it will return/update an collection of vinyl. Each IMusicRepository
consumer, for example MusicCatalogService
, does not know anything about VinylCatalog
: this is one of the main aims of the SOLID programming principles and Dependency injection. The following code shows the typescript implementation of the previous UML schema:
//@file Track.ts
export class Track{
constructor(id: number, title: string, artist: string, duration:number){
this.Id= id;
this.Title= title;
this.Artist= artist;
this.Duration= duration;
}
public Id : number;
public Title: string;
public Artist: string;
public Duration: number;
}
//@file IMusicRepository.ts
import { Track } from "../Models/Track";
export interface IMusicRepository {
get() : Track[];
getById(id: number) : Track;
add(track: Track) : number;
edit(id: number, track: Track) : Track;
delete(id: number) : Track;
}
//@file VinylCatalog.ts
import {IMusicRepository} from "./IMusicRepository";
import { Track } from "../Models/Track";
export class VinylCatalog implements IMusicRepository{
private vinylList : Track[] = new Array(
new Track(1, "DNA.", "Kendrick Lamar", 340),
new Track(2, "Come Down", "Anderson Paak.", 430),
new Track(3, "DNA.", "Kendrick Lamar", 340),
new Track(4, "DNA.", "Kendrick Lamar", 340),
new Track(5, "DNA.", "Kendrick Lamar", 340)
);
get(): Track[] {
return this.vinylList;
}
getById(id: number): Track {
return this.vinylList.find(track=> track.Id== id);
}
add(track: Track): number {
return this.vinylList.push(track);
}
edit(id: number, track: Track): Track {
var targetIndex = this.vinylList.findIndex((track => track.Id == id));
this.vinylList[targetIndex].Artist= track.Artist;
this.vinylList[targetIndex].Title= track.Title;
this.vinylList[targetIndex].Duration= track.Duration;
return this.vinylList[targetIndex];
}
delete(id: number): Track {
var targetIndex = this.vinylList.findIndex((track => track.Id == id));
if (targetIndex < -1) return null;
return this.vinylList.splice(targetIndex, 1)[0];
}
}
//@file MusicCatalogService.ts
import { IMusicRepository } from "../Repositories/IMusicRepository";
import { Track } from "../Models/Track";
export class MusicCatalogService{
private repository: IMusicRepository;
constructor(repository:IMusicRepository){
this.repository= repository;
}
get(): Track[] {
return this.repository.get();
}
getById(id: number): Track {
return this.repository.getById(id);
}
add(track: Track): number {
return this.repository.add(track);
}
edit(id: number, track: Track): Track {
return this.repository.edit(id, track);
}
delete(id: number): Track {
return this.repository.delete(id);
}
}
Finally, we are ready to setup InversifyJs, which is our Dependency injection container. The main aims of InversifyJS is to register and map interfaces with concrete classes. There is always a key component when we talk about Inversion of control container: Installer. The installer provides mapping between interfaces and their concrete classes. We can find installer concept in every DI container framework and in every language, from Javascript to C#. Let's create the Installer module:
//@file Installer.ts
import "reflect-metadata";
import { Container } from "inversify";
import SERVICE_IDENTIFIER from "../Constants/Identifiers";
import {IMusicRepository} from "../Repositories/IMusicRepository";
import {VinylCatalog} from "../Repositories/VinylCatalog";
let container = new Container();
container.bind<IMusicRepository>(SERVICE_IDENTIFIER.IMusicRepository).to(VinylCatalog);
export default container;
@ line 9 we can find the mapping between the IMusicRepository
and VinylCatalog
. InversifyJs also require to decorate our concrete class with the @injectable
attribute, like this:
//@file VinylCatalog.ts
import {IMusicRepository} from "./IMusicRepository";
import { Track } from "../Models/Track";
import { inject, injectable, named } from "inversify";
@injectable()
export class VinylCatalog implements IMusicRepository{
private vinylList : Track[] = new Array(
new Track(1, "DNA.", "Kendrick Lamar", 340),
new Track(2, "Come Down", "Anderson Paak.", 430),
new Track(3, "DNA.", "Kendrick Lamar", 340),
new Track(4, "DNA.", "Kendrick Lamar", 340),
new Track(5, "DNA.", "Kendrick Lamar", 340)
);
get(): Track[] {
return this.vinylList;
}
getById(id: number): Track {
return this.vinylList.find(track=> track.Id== id);
}
add(track: Track): number {
return this.vinylList.push(track);
}
edit(id: number, track: Track): Track {
var targetIndex = this.vinylList.findIndex((track => track.Id == id));
this.vinylList[targetIndex].Artist= track.Artist;
this.vinylList[targetIndex].Title= track.Title;
this.vinylList[targetIndex].Duration= track.Duration;
return this.vinylList[targetIndex];
}
delete(id: number): Track {
var targetIndex = this.vinylList.findIndex((track => track.Id == id));
if (targetIndex < -1) return null;
return this.vinylList.splice(targetIndex, 1)[0];
}
}
At least, we can implement our composition root (It will be implemented in a main file for demo purpose):
//@file: main.ts
import {IMusicRepository} from "./Repositories/IMusicRepository";
import container from "./Infrastructure/Installer";
import SERVICE_IDENTIFIER from "./Constants/Identifiers";
import { MusicCatalogService } from '../src/Services/MusicCatalogService';
// Composition root
let musicRepoo = container.get<IMusicRepository>(SERVICE_IDENTIFIER.IMusicRepository);
let service= new MusicCatalogService(musicRepoo);
console.log(service.get());
Unit Testing all the things
Our loose coupling structure facilitates unit testing over our services and classes. The following example will use  Jest as unit test framework. Jest is a testing platform powered by Facebook, it is used by Facebook to test all JavaScript code including React applications. Jest also offers an mocking built-in library, it will be useful in our demo to mock up the repository functions. Let's get started by testing the MusicCatalogService.get
method:
//@file MusicCatalogService.spec.ts
import { MusicCatalogService } from '../src/Services/MusicCatalogService';
import { IMusicRepository } from '../src/Repositories/IMusicRepository';
import { Track } from '../src/Models/Track';
describe('MusicCatalogService tests', () => {
let sut: MusicCatalogService;
let mockRepo: Track[] = new Array(
new Track(1, "Mock Title 1", "The Mockers", 0),
new Track(2, "Mock Title 2", "The Mockers 2", 0)
);
it('Should return Tracks value', () => {
//Arrange
const Mock = jest.fn<IMusicRepository>(() => ({
get: jest.fn().mockReturnValue(mockRepo)
}));
const mock = new Mock();
sut = new MusicCatalogService(mock);
//Act
var result = sut.get();
//Assert
expect(mock.get).toHaveBeenCalled();
expect(result.length).toBe(2);
});
});
The previous test covers the MusicCatalogService.get
. First of all, it generates the mock result for the method get
. Secondly, it initialize the MusicCatalogServices
by using the generated mock. Finally, we can test  others method of MusicCatalogService
by using the same pattern:
//@file MusicCatalogService.spec.ts
import { MusicCatalogService } from '../src/Services/MusicCatalogService';
import { IMusicRepository } from '../src/Repositories/IMusicRepository';
import { Track } from '../src/Models/Track';
describe('MusicCatalogService tests', () => {
let sut: MusicCatalogService;
let mockRepo: Track[] = new Array(
new Track(1, "Mock Title 1", "The Mockers", 0),
new Track(2, "Mock Title 2", "The Mockers 2", 0)
);
it('Should return Tracks value', () => {
//Arrange
const Mock = jest.fn<IMusicRepository>(() => ({
get: jest.fn().mockReturnValue(mockRepo)
}));
const mock = new Mock();
sut = new MusicCatalogService(mock);
//Act
var result = sut.get();
//Assert
expect(mock.get).toHaveBeenCalled();
expect(result.length).toBe(2);
});
it('Should return Tracks by id', () => {
//Arrange
const Mock = jest.fn<IMusicRepository>(() => ({
getById: jest.fn().mockReturnValue(mockRepo[0])
}));
const mock = new Mock();
sut = new MusicCatalogService(mock);
//Act
var result = sut.getById(1);
//Assert
expect(mock.getById).toHaveBeenCalled();
expect(result.Id).toBe(1);
expect(result.Title).toBe("Mock Title 1");
});
it('Should add Track', () => {
//Arrange
const Mock = jest.fn<IMusicRepository>(() => ({
add: jest.fn().mockImplementation(
(track : Track)=>{
return mockRepo.push(track);
}
)
}));
const mock = new Mock();
sut = new MusicCatalogService(mock);
//Act
var result = sut.add(new Track(3,"Track Test", "Track test",0));
//Assert
expect(mock.add).toHaveBeenCalled();
expect(result).toBe(3);
});
it('Should edit Track', () => {
//Arrange
const Mock = jest.fn<IMusicRepository>(() => ({
edit: jest.fn().mockImplementation(
(id: number, track : Track)=>{
return track;
}
)
}));
const mock = new Mock();
sut = new MusicCatalogService(mock);
//Act
var result = sut.edit(1, new Track(1,"Track Test", "Track test",0));
//Assert
expect(mock.edit).toHaveBeenCalled();
expect(result.Title).toBe("Track Test");
});
it('Should delete Track', () => {
//Arrange
const Mock = jest.fn<IMusicRepository>(() => ({
delete: jest.fn().mockImplementation(
(id: number)=>{
var targetIndex = mockRepo.findIndex((track => track.Id == id));
return mockRepo.splice(targetIndex, 1)[0];
}
)
}));
const mock = new Mock();
sut = new MusicCatalogService(mock);
//Act
var result = sut.delete(1);
//Assert
expect(mock.delete).toHaveBeenCalled();
expect(result.Title).toBe("Mock Title 1");
});
});
Final thought
You can find the following demo on GitHub. In conclusion, I think we should follow those suggestions:
- stop thinking that IoC containers have no place in JavaScript applications;
- start writing Object-oriented JavaScript code that follows the SOLID principles;
For more informations:
SOLID principles using Typescript
Dependency Injection overview
Cheers :)
Top comments (8)
If you wanted to run this app, how are you able to get your
VinylCatalog
injected into yourMusicCatalogService
without using the@inject
decorator in the constructor ofMusicCatalogService
?I have a service where, at runtime I have bound to the container at runtime the concrete classes, and if I dont use the @inject decorator, I get runtime errors. If I leave them, and try to pass in mock objects to the constructor, I get an error. How can I get both runtime and test working?
Hi Jhon,
thank you for the interest, can you send me a git hub repo or paste bin?
I will check as soon as possibile :)
Sem
Ah this is such a great topic to cover for JavaScript developers.
Thank you for the article.
There's also a library called "container-ioc" and is also for Javascript/Typescript & Node.js apps.
It has almost 1 to 1 Angular 4 API and makes use of providers for registration.
I found it more flexible and easier to use in most of my projects.
npmjs.com/package/container-ioc
Hi, I will definitely try it.
Thank you for the advice :)
Samuele
Thanks a lot for writing about InversifyJS, great article :)
Great!. Coincidently I wrote a similar article last week!
dev.to/theodesp/understanding-soli...
Thanks for sharing this. What is the "composition root" that you mentioned in the middle of the article?