How to Implement Dependency Injection in TypeScript Like a Pro

Nolan
5 min readJun 29, 2024

--

If you had experience working on large projects, you may notice that managing tons of classes and their dependencies gets really complicated and whenever a class requires a new dependency or when a class doesn’t need a dependency anymore, in both cases you need to make a lot of changes. In small and medium projects, you may don’t feel anything but in large projects it’s kind of impossible! If you look at Nest.js (a backend framework in Nodejs) you see everything just works beautifully because you just need to make a class as dependency and Nest.js does its magic. Can we do it without using Nest.js directly? Why not? Let’s do that… 😎

DI (dependency injection) vs DIP (dependency inversion principle)? What’s the difference?

In software engineering world, we have two closely related principles that you may be confused but don’t worry, I’m here to make these two principles clear for you.

DIP is an important principle in OOP world and it’s part of SOLID principle. It’s important to consider it when you design classes and relations, but you know what exactly says this principle?

class Notificaiton {
public notify() {
throw new Error("This method is not implemented!");
}
}

class PushNotification {
public notify() {
// your logic for sending push notification
}
}

class EmailNotification {
public notify() {
// your logic for sending email notification
}
}

class UserService {
constructor(private notification: Notification) {}

createUser() {
// your logic for creating a new user in Database
this.notification.notify();
}
}

As you see UserService requires an instance of Notification class but which one of subclasses? That is exactly the point!

High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces) — Wikipedia.

DI is another important and popular design and in my opinion, it aims to make us don’t forget SRP (single responsibility principle)! Take a look at this example:

// in user.service.ts
class UserService {
constructor() {}

createUser() {
// logic
}

deleteUser() {
// logic
}
}

// in user.controller.ts
import {UserService} from 'user.service';

class UserController {
constructor(userService: UserService) {}

createAccount() {
// your controller logic
}

deleteAccount() {
// your controller logic}
}
}

As you see, UserController is not responsible to create an instance of UserService but instead it is a consumer and requires it in constructor.

Dependency injection aims to separate the concerns of constructing objects and using them, leading to loosely coupled programs — Wikipedia.

You need DIC (dependency injection container) to take advantages of DI (dependency injection)!

If you inject dependencies manually, it’s time to level-up your code quality. DIC is a tool that instantiate and configure your classes and you only have to request the class that you need an instance of it! Personally, I use Tsyringe which is developed by Microsoft and is super easy to start with.

Before jumping into it, you may ask “When should we use DIC tools?” but unfortunately there is no specific rule. I prefer you to don’t get things complicated until you’re sure about the future of your code. DIC is suitable for projects that you have to struggle with a large number of different objects. To show you how Tsyringe is going to help us, I’ll show you the usage in practice.

Let’s walk through a real example starting with directory structure:

root
| package-lock.json
| package.json
| tsconfig.json
| node_modules
+---src
| index.ts
|
+---controllers
| | index.ts
| |
| +---auth
| auth.controller.ts
| auth.service.ts
| index.ts
|
+---enums
| routers.ts
|
+---routers
auth.ts
index.ts

⚠️ Make sure you have enabled emitDecoratorMetadata and experimentalDecorators options in your tsconfig.json.

1. Directory /controllers

First of all, we will take a look at controller's directory. Each controller is considered to be a module just like Nest.js (which contains service and controller files). By navigating to /controllers/auth/auth.controller.ts we have these lines of codes:

import { decorators } from "tsyringe";
import { UserService } from "./auth.service";
import { Request, Response } from "express";

@decorators.singleton()
export class AuthController {
constructor(private userService: UserService) {}

async login(request: Request, response: Response) {
let { username, password } = request.body;
await this.userService.login({ password, username });
response.status(200).json({ msg: "Login successfully!" });
}
}

So, what does the singleton() decorator do? This decorator is going to register AuthController in the container. In that way I can later tell the container to get me an instance of AuthController whenever I need like this:

import {container} from 'tsyringe';
let instance = container.resolve(AuthController);

But as the name suggests, when you decorate a class with the singleton() decorator, you are using Singleton (a design pattern) and you will get the same instance each time you call resolve() method. You maybe want to get a new instance each time, so there is another decorator named injectable() that is just like singleton() decorator but instead a new instance will create each time you resolve it! Alright, Let’s continue and see what is happening in /controllers/auth/auth.service.ts:

import { decorators } from "tsyringe";

@decorators.injectable()
export class UserService {
constructor() {}

async login(input: { username: string, password: string }) {
/**
* Your logic here
*/
return {};
}
}

As you see, there is a simple implementation of service just for illustrating.

2. Directory /routers

Routers are responsible to route requests to destination controllers. Let’s see how we implemented our router that is located in /routers/auth.router.ts:

import express from 'express';
import { container } from 'tsyringe';
import { AuthController } from '../controllers'
import { Routers } from '../enums/routers';

let router = express.Router();
let controller = container.resolve(AuthController);

router.post(
'/auth/login',
controller.login.bind(controller)
);

/** Register `router` instance to tsyringe */
container.registerInstance(Routers.AuthRouter, router);

Everything is clear if you be familiar with Express.js, but you probably noticed that the router isn’t exported at the end of file. Well, you are free to just use import-export model and because I wanna show you different functionalities that Tsyringe provides we are going to use registerInstance().

But what does it do? Because we instantiated from our router earlier in line 6, so it’s pointless to do it again. For this, Tsyringe provides a functionality to register instances to the container manually. As you see it takes two arguments; the first argument is the key that we want to access to the instance later and the second one is the instance itself.

3. File /index.ts

And finally, let’s navigate to the index.ts where is the entry point of our tiny app:

import './routers';
import express from 'express';
import { container } from 'tsyringe';
import { Routers } from './enums/routers';

let app = express();
app.use(express.json());

/** <<<< Routes >>>> */
app.use(container.resolve(Routers.AuthRouter));

/** <<<< Listen to port >>>> */
let PORT = parseInt(process.env.PORT || '3000');
app.listen(PORT, () => {
console.log('Server started at: %s', `http://127.0.0.1:${PORT}`);
});

Alright, as we expected, AuthRouter has been resolved by the container using the key by which we registered the router.

Okey let’s run the project to see the power of Tsyringe! But before being able to run it, don’t forget to compile your code to JavaScript :)

npx tsc && node dist/index.js

By send a POST request to the route http://<host>:<port>/auth/login you will see it works with no problem, beautiful right? If it was helpful, don’t forget to leave a comment :) see you in the next story👋😉

Project Repository: https://github.com/alireza12prom/medium-how-to-implement-dependency-injection-like-a-pro.git

--

--

Nolan
0 Followers

Let me drink my coffee first! ☕😎