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;

Â 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;

Â 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 :)