Dependency Injection in TypeScript

In modern programming languages, such as JavaScript and TypeScript, dependency injection (DI) has become a fundamental concept. TypeScript, in particular, has gained significant popularity in recent years, and developers often seek ways to incorporate DI into their TypeScript code. In this blog post, we will explore how to leverage the “tsyringe” library to implement DI without decorators in TypeScript.

What is tsyringe?

Tsyringe is a lightweight dependency injection container for TypeScript and JavaScript that supports constructor injection. By using tsyringe, developers can easily manage and resolve dependencies in their applications.

Scenario:

Let’s consider a scenario where we have two services, FooService and BarService, each implementing their respective interfaces, IFoo and IBar.

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("Inside Bar Service");
    }
}

Consuming Services without DI:

In our main application, App.ts, we manually instantiate the dependencies and pass them to the constructor.

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

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();

Drawbacks of Tight Coupling:

This approach tightly couples the dependencies to the application code, limiting extensibility, reusability, and making adjustments difficult. It also hinders effective testing.

Implementing DI with tsyringe:

To eliminate tight coupling, we can introduce DI using the tsyringe library. Let’s install tsyringe by running the following command:

npm install tsyringe

Next, we need to add the @injectable decorator to each service class:

import { injectable } from "tsyringe";

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

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

Registering Dependencies:

We register the dependencies with the tsyringe container:

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

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

Resolving Dependencies:

Finally, we use the container to resolve the dependencies in the constructor:

import { container } from "tsyringe";

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();

Using Dependency Injection:

To demonstrate a more complex scenario, let’s modify the example. We’ll pass the BarService dependency to the FooService constructor:

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("Inside Foo Service");
    }
}

To address potential circular dependency problems, we can use the delay function as a helper:

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("Inside Foo Service");
    }
}

Conclusion:

In this blog post, we explored the benefits of incorporating dependency injection into TypeScript applications. We learned how to use the tsyringe library to implement DI without decorators, thus reducing tight coupling and improving code maintainability. By leveraging DI, developers can enhance the extensibility, reusability, and testability of their TypeScript code.

To delve deeper into tsyringe, refer to the official documentation.

Next Post Previous Post
No Comment
Add Comment
comment url