In this article, we explain how to implement Dependency Injection in TypeScript using the InversifyJS library. The goal is to elucidate the benefits of Dependency Injection and to demonstrate how to make your code more efficient and maintainable by using InversifyJS.
What is Dependency Injection in TypeScript?
Dependency Injection (DI) is a design pattern that reduces coupling between classes (Loose Coupling). It allows classes to communicate via interfaces or abstractions rather than directly interacting. DI increases the testability and flexibility of code. In TypeScript, InversifyJS offers a powerful library that simplifies this implementation.
Why Dependency Injection?
Without Dependency Injection, classes are often tightly bound to specific implementations. This complicates future changes and increases the potential for errors. DI solves this problem by managing dependencies centrally and providing them automatically.
Dependency Injection in TypeScript with a Weather Forecasting System
As an example, we will develop a small weather forecasting system with two different forecasters: "WeatherSatellite" and "WeatherStation". Both forecasters implement weather forecasting, but we want to design the system so that the specific forecasting method remains flexible.
Example without Dependency Injection
class WeatherSatellite {
getTemperature(): number {
return 20;
}
}
class WeatherStation {
getTemperature(): number {
return 22;
}
}
class Person {
private weatherSatellite: WeatherSatellite;
constructor() {
this.weatherSatellite = new WeatherSatellite();
}
}
UML Diagram: Current Design without Dependency Injection
Problem: The Person
class decides which weather forecasting method to use. This leads to strong coupling and makes it difficult to change or extend the implementation.
Improvement: Introduction of Interfaces
In TypeScript, we can use interfaces to abstract dependencies. Here we define an IWeatherForecasting
interface:
interface IWeatherForecasting {
getTemperature(): number;
}
class WeatherSatellite implements IWeatherForecasting {
getTemperature(): number {
return 20;
}
}
class WeatherStation implements IWeatherForecasting {
getTemperature(): number {
return 22;
}
}
class Person {
private weatherForecasting: IWeatherForecasting;
constructor(weatherForecasting: IWeatherForecasting) {
this.weatherForecasting = weatherForecasting;
}
}
UML Diagram: Interface Implementation without Dependency Injection
Problem: Even though an interface is now used, the Person
class still decides which implementation to use. This contradicts the DI principle.
Solution: Dependency Injection with InversifyJS
To fully resolve this dependency, we use InversifyJS to automatically inject the required objects.
Code Example: Using InversifyJS for Dependency Injection
First, we need to install InversifyJS and reflect-metadata:
npm install inversify reflect-metadata
Then create the TypeScript code with InversifyJS:
import 'reflect-metadata';
import {Container, injectable, inject} from 'inversify';
const TYPES = {
IWeatherForecasting: Symbol.for('IWeatherForecasting')
};
interface IWeatherForecasting {
getTemperature(): number;
}
@injectable()
class WeatherSatellite implements IWeatherForecasting {
getTemperature(): number {
return 20;
}
}
@injectable()
class WeatherStation implements IWeatherForecasting {
getTemperature(): number {
return 22;
}
}
@injectable()
class Person {
private weatherForecasting: IWeatherForecasting;
constructor(@inject(TYPES.IWeatherForecasting) weatherForecasting: IWeatherForecasting) {
this.weatherForecasting = weatherForecasting;
}
getForecast(): number {
return this.weatherForecasting.getTemperature();
}
}
const container = new Container();
container.bind<IWeatherForecasting>(TYPES.IWeatherForecasting).to(WeatherSatellite);
const person = container.resolve(Person);
console.log(person.getForecast()); // Output: 20
Explanation
- @injectable: Decorator that makes the class available for Dependency Injection.
- @inject: Allows the injection of dependencies into the class constructor.
- Container: Defines which implementations are used for the respective dependencies.
UML Diagram: Design with Dependency Injection
Advantage
The Person
class is now completely independent of the weather forecasting implementation. Changes to the implementation can easily be made in the DI container.
Extending the DI Container
It is also possible to add other implementations or dependencies to the container by using different bindings with .to()
.
Frequently Asked Questions about Dependency Injection
Why should I use InversifyJS instead of manual Dependency Injection?
InversifyJS simplifies the management of dependencies and makes the code more modular. Manually managing dependencies can lead to errors and hard-to-maintain code, especially in larger applications.
Can InversifyJS also be used in frontend frameworks like Angular?
Yes, InversifyJS can be used both in the backend (Node.js) and in the frontend with frameworks like Angular or React. However, it is important to check whether the chosen DI approach can be well integrated into the respective framework.
Conclusion
Dependency Injection in TypeScript with the InversifyJS library helps to make code more modular, flexible, and maintainable. InversifyJS enables easy management of dependencies, making the code easier to adapt and extend.