Dependency Injection in TypeScript from scratch using TSyringe

Any language now acknowledges dependency injection as a first-class citizen. Javascript(typescript) has seen a lot of development in recent years. Typescript has risen to the top of the language charts.
In this article, I’ll show you how to use a “tsyringe” to add DI to your typescript code.

TSyringe. A lightweight dependency injection container for TypeScript/JavaScript for constructor injection.

In this post, I will show how to add DI in your typescript application without decorators.

Consider the following scenario:

export interface IFoo{

    doSomething():void;
}

export class FooService implements IFoo{
    doSomething(): void {
        console.log("Inside Foo Service");
    }
}


export interface IBar {
  doSomethingReal(): void;
}

export class BarService implements IBar {
  doSomethingReal(): void {
    console.log("Inisde Bar Service");
  }
}

Let’s consume these services in our “main” application. In our case “App.ts”.

import { IBar, BarService } from "./Bar";
import { FooService, IFoo } from "./Foo";

export class App {
  private _foo: IFoo;
  private _bar: IBar;
  constructor(foo: IFoo, bar: IBar) {
    this._foo = foo;
    this._bar = bar;
  }

  run(): void {
    this._foo.doSomething();
    this._bar.doSomethingReal();
  }
}

const app = new App(new FooService(), new BarService());
app.run();

In App.ts, you can see that I’m manually passing the dependency.

Tight coupling is poor in almost every situation because it limits code extensibility and reusability, makes the system brittle, and makes programme adjustments extremely difficult. It makes testing more challenging as well.

By incorporating DI into our system, you can eliminate the dependency. The DI system eliminates the code’s tight coupling. You can either write your own DI or use some other publicly available DI system. I’m using the tsyringe library in this tutorial.

Let’s install tysringe by running the following command

npm install tysringe

And add @injectable decorator on each service in out case Foo and Bar Service



@injectable()
export class BarService implements IBar {
  ...
}


@injectable()
export class FooService implements IFoo {
  ...
}

Now let’s register the dependecny to container


import "reflect-metadata";
import { container } from "tsyringe";
...

container.register("Foo",FooService);
container.register("Bar",BarService);

Then call the container.resolve in the constructor

App.ts

import "reflect-metadata";
import { FooService, IFoo } from './Foo';
import { container } from "tsyringe";
import { BarService, IBar } from './Bar';

container.register("Foo",FooService);
container.register("Bar",BarService);
export class App {
  private _fooService: IFoo;
  private _barService:IBar;

  constructor() {
    this._fooService = container.resolve("Foo");
    this._barService = container.resolve("Bar");
  }

  run(): void {
    this._fooService.doSomething();
    this._barService.doSomethingReal();
  }
}

const app = new App();
app.run();


Let’s change the example little bit. Now pass the BarService into FooService

Foo.ts

import { delay, inject, injectable } from "tsyringe";
import { IBar, BarService } from "./Bar";

export interface IFoo {
  doSomething(): void;
}

@injectable()
export class FooService implements IFoo {
  private _barService: IBar;
  constructor(barService: IBar) {
    this._barService = barService;
  }
  doSomething(): void {
    this._barService.doSomethingReal();
    console.log("Inisde Foo Service");
  }
}


If you try to run the application now, you’ll get the following error.

Error: Cannot inject the dependency at position #0 of “Foo” constructor. Reason:
Attempted to construct an undefined constructor. Could mean a circular dependency problem. Try using delay function.

App.ts

import "reflect-metadata";
import { FooService, IFoo } from "./Foo";
import { container } from "tsyringe";

export class App {
  private _foo: IFoo;

  constructor() {
    this._foo = container.resolve(FooService);
  }

  run(): void {
    this._foo.doSomething();
  }
}

const app = new App();
app.run();


To solve this issue you can use the delay function helper. The delay function wraps the constructor in an instance of DelayedConstructor.

Foo.ts

import { delay, inject, injectable } from "tsyringe";
import { IBar, BarService } from "./Bar";

export interface IFoo {
  doSomething(): void;
}

@injectable()
export class FooService implements IFoo {
  private _barService: IBar;
  constructor(@inject(delay(() => BarService)) barService: IBar) {
    this._barService = barService;
  }
  doSomething(): void {
    this._barService.doSomethingReal();
    console.log("Inisde Foo Service");
  }
}


When you run the application now, there will be no errors.

If you want to learn more about tysringe go through this link

Please do not post any spam link in the comment box😊

Post a Comment (0)
Previous Post Next Post