• Dependency Injection in TypeScript
  • Mert Turkmeno ğlu
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: Usualminds
  • Proofread by Kim Yang, PassionPenguin

Dependency injection in TypeScript

Introduction to the

Every software program has its most basic building blocks. In object-oriented programming languages, we use classes to build complex architectures. Like building a building, we call the connections between modules dependencies. Other classes provide complex encapsulation operations to support the needs of our class.

A class may have fields that reference other classes. Therefore, we have to ask these questions: how are these references created? Do we compose these objects, or do other classes instantiate them? What if the class we’re instantiating is too complex and we want to avoid junk code? All of these problems can be attempted through the dependency injection principle.

Before we begin the example, we must understand some concepts related to dependency injection. The dependency injection principle tells us that a class should receive, not instantiate, its dependencies. Object initialization is done by delegation, which can handle more complex operations and reduce the stress of class design. You can remove complex modules from your code and reintroduce dependencies in other ways. How to handle removing and reintroducing dependencies is a dependency management issue. You can handle the initialization and injection of all objects manually, but that would complicate the system and we want to avoid that. Instead, you can transfer the construction responsibility to the IoC container.

Inversion of control is done by reversing the flow of the entire program so that the container manages all dependencies involved in the program. You can create a container, and the entire container is responsible for constructing objects. When a class needs to instantiate an object, the IoC container can provide the dependencies it needs.

IoC provides a method rather than an implementation. To use the dependency injection principle, you need a dependency injection framework. Examples are as follows:

  • Spring and Dagger are Java’s dependency injection frameworks
  • Hilt is Kotlin’s dependency injection framework
  • Unity is C#’s dependency injection framework
  • Inversify, Nest.js, and TypeDI are TypeScript dependency injection frameworks

Overview and character division

In the dependency injection principle, we need to understand four different roles:

  • The client
  • The service side
  • interface
  • injector

The server side is what we use to expose services. These classes are instantiated and used by the IoC container. A client uses these services through an IoC container. The client should not be bogged down in details, so the interface needs to ensure that the client and server are in harmony. The client requests the required dependencies, and the injector provides the instantiation service.

Type of dependency injection

When we discuss how to inject dependencies into a class, we can do this in three different ways:

  • We can provide dependencies through properties (fields). Defining a property on a class and then injecting a concrete object into that property is called property injection. By exposing this property, it violates the encapsulation principle of object-oriented programming. Therefore, avoid this injection as much as possible.
  • We can provide dependencies through methods. The state of an object should be private, and when the outside world wants to change that state, it should call the getter/setter methods of the class. So when you use setter methods to initialize private fields in a class, you can use method injection.
  • We can provide dependencies through constructors. Constructor methods are highly integrated because of their basic properties and object construction. We generally support injection through constructors because our goal is similar to the constructor’s approach.

Using TypeDI library

Once we understand the basics of dependency injection, it doesn’t make much difference what framework or library we use. In this article, I’ve chosen the TypeScript language and TypeDI libraries to demonstrate these basic concepts.

It takes time to initialize Yarn and add TypeScript. I don’t want to use a well-known project configuration that doesn’t have enough comments, because that would be boring. So I’ll give you the initial code and a brief introduction. You can view and download code from the Github repository.

Any TypeScript project can serve as an example of a dependency injection demonstration. But in this article I chose a Node/Express application as an example. I assume that developers who use TypeScript either directly use Node/Express servers or know something about them.

When you look at the package.json file, you can see these dependency configurations. Let me briefly describe them:

  • Express: Express is a popular framework for writing Node.js RESTful services.
  • Reflect-metadata: a library for the metadata reflection API. It allows other libraries to use metadata through decorators.
  • Ts-node: Node.js cannot run TypeScript files. TypeScript needs to be compiled into JavaScript before the code can run. Ts-node handles this process for you.
  • Typedi: Typedi is a TypeScript dependency injection library. We’ll see an example of this shortly.
  • Typescript: We use typescript in this project, so we need to make it a dependency as well.
  • @types/ Express: Type definition of the Express library.
  • @types/node: The type definition of Node.js.
  • Ts-node-dev: This library allows you to run TypeScript and see how certain files change.

There are some important compiler option configurations that you need to be aware of. If you look at tsconfig.json, you can see the configuration options for the compilation process:

  • We specify types for reflect-metadata and node.
  • We must set mitDecoratorMetadata and experimentalDecorators to true.

All the source code is in the SRC folder. SRC /index.ts is the entry file for our project. This file contains all the boot steps for the server:

import 'reflect-metadata';

import express from 'express';
import Container from 'typedi';
import UserController from './controllers/UserController';

const main = async() = > {const app = express();

  const userController = Container.get(UserController);

  app.get('/users'.(req, res) = > userController.getAllUsers(req, res));

  app.listen(3000.() = > {
    console.log('Server started');
  });
}

main().catch(err= > {
  console.error(err);
});
Copy the code

This code is a small Express server with only one port. When you send a GET request to the/Users route, it returns a list of users. At the heart of the main function is the container.get method. Note that we are not using the new keyword or instantiating the object. We simply call a UserController instance method returned by the IoC container. The routing and controller methods are then bound.

Our application is a virtual RESTful server, but I don’t want it to be meaningless. I’ve added four different folders that represent the basics of a complete back-end service. These are controllers, Models, Repositories, and Services. Now let me introduce them one by one:

  • The Controllers folder contains our REST Controllers. They are responsible for coordinating communication between clients and servers. They receive requests and return responses.
  • The Models folder contains our database entity classes. We don’t have a database connection, nor do we need one, but having a proper project structure is a great help in learning about the project. Let’s assume it’s a real database entity and continue with our project.
  • The Services folder contains our Services. They are responsible for providing the required services to REST controllers by accessing different repositories.
  • The Repositories folder contains our database connection class. We use the Data Mapper schema to perform database operations. In this pattern, we use entity classes to access the database and perform related operations.

We’re not going to put everything in one class. There are many levels between request and response. This is called a layered architecture. By sharing dependencies between classes, we can do dependency injection more easily.

import { Request, Response } from "express";
import { Service } from "typedi";
import UserService from ".. /services/UserService";

@Service(a)class UserController {
  constructor(private readonly userService: UserService){}async getAllUsers(_req: Request, res: Response) {
    const result = await this.userService.getAllUsers();
    returnres.json(result); }}export default UserController;
Copy the code

UserController has only one method. The getAllUsers method is responsible for getting the results from the user service and transferring them. We add a Service decorator to the UserController class because we want this class to be managed by the IoC container. Inside the constructor method, we can see that this class needs a UserService instance. Again, we don’t need to control this dependency. Because the TypeDI container creates an instance of UserService, when it generates a UserController instance, it will be injected into UserService.

import { Service } from "typedi";
import User from ".. /models/User";
import UserRepository from ".. /repositories/UserRepository";

@Service(a)class UserService {
  constructor(private readonly userRepository: UserRepository){}async getAllUsers(): Promise<User[]> {
    const result = await this.userRepository.getAllUsers();
    returnresult; }}export default UserService;
Copy the code

UserService is very similar to UserController. We add a Service decorator to the class and specify the dependencies they want in the constructor method.

import { Service } from "typedi";
import User from ".. /models/User";

@Service(a)class UserRepository {
  private readonly users: User[] = [
    { name: 'Emily' },
    { name: 'John' },
    { name: 'Jane'},];async getAllUsers(): Promise<User[]> {
    return this.users; }}export default UserRepository;
Copy the code

UserRepository is our final step. We annotate this class with Service, but we don’t have any dependencies. Since there is no database connection, I simply add the hard-coded user list to the class as a private property.

conclusion

Dependency injection is a powerful tool for managing initialization of complex objects. Doing dependency injection manually is better than doing nothing, but using TypeDI is simpler and more feasible. When starting a new project, you should definitely think about the dependency injection principle and give it a try.

You can find the code for this article on the GitHub branch.

You can find me on GitHub, LinkedIn and Twitter.

Thanks for reading and have a great day.

reference

  • [1] www.tutorialsteacher.com/ioc/depende…
  • [2] en.wikipedia.org/wiki/Depend…
  • [3] developer.android.com/training/de…
  • [4] stackoverflow.com/questions/2…
  • [5] docs.typestack.com munity/typedi/v/DE…

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.