DEV Community

Cover image for dependency injection in typescript using di-injectable library
farajshuaib
farajshuaib

Posted on

dependency injection in typescript using di-injectable library

In this article, we’ll explore the concept of dependency injection in TypeScript and how it can revolutionize our software development process using di-injectable library.

What is Dependency Injection?

Dependency injection is a design pattern that allows us to decouple components by injecting their dependencies from external sources rather than creating them internally. This approach promotes loose coupling, reusability, and testability in our codebase.

Constructor Injection

Constructor injection is one of the most common forms of dependency injection. It involves injecting dependencies through a class’s constructor. Let’s consider an example:

class UserService {
  constructor(private userRepository: UserRepository) {}

  getUser(id: string) {
    return this.userRepository.getUserById(id);
  }
}

class UserRepository {
  getUserById(id: string) {
    // Retrieve user from the database
  }
}

const userRepository = new UserRepository();
const userService = new UserService(userRepository);
Enter fullscreen mode Exit fullscreen mode

In the above example, the UserService class depends on the UserRepository class. By passing an instance of UserRepository through the constructor, we establish the dependency between the two classes. This approach allows for easy swapping of different implementations of UserRepository, making our code more flexible and extensible.

Benefits of Dependency Injection

By embracing dependency injection, we unlock several benefits that greatly enhance our codebase:

  • Loose Coupling
    Dependency injection promotes loose coupling between components, as they depend on abstractions rather than concrete implementations. This enables us to swap out dependencies easily, facilitating code maintenance and scalability.

  • Reusability
    With dependency injection, we can create components with minimal dependencies, making them highly reusable in different contexts. By injecting specific implementations of dependencies, we can tailor the behavior of a component without modifying its code.

  • Testability
    Dependency injection greatly simplifies unit testing. By injecting mock or fake dependencies during testing, we can isolate components and verify their behavior independently. This leads to more reliable and maintainable test suites.

  • Flexibility and Extensibility
    Using dependency injection allows us to add new features or change existing ones without modifying the core implementation. By injecting new dependencies or modifying existing ones, we can extend the functionality of our codebase without introducing breaking changes.

lets make dependency injection easier by using DI-injectable library

DI-injectable library is a simple Dependency Injection (DI) library for TypeScript supporting Singleton and Transient service lifetimes.

Installation

First, install the package via npm or yarn:


npm install injectable

yarn add injectable

Enter fullscreen mode Exit fullscreen mode

Usage

Setting Up Services

  • Define Services: Create your service classes and use the @Injectable decorator and use ServiceLifetime enum to register your services as Singleton or Transient..
  • Resolve Services: Use the ServiceProvider to resolve instances of your services.

Example

Let's walk through a complete example.

  1. Define Services Create some simple services and use the @Injectable decorator.
// src/services/logger.ts
import { Injectable } from 'di-injectable'; 

@Injectable(ServiceLifetime.Singleton)
export class Logger {
  log(message: string) {
    console.log(`Logger: ${message}`);
  }
}
Enter fullscreen mode Exit fullscreen mode
// src/services/userService.ts
import { Injectable, Inject } from 'di-injectable';
import { Logger } from './logger';

@Injectable()
export class UserService {
  constructor(@Inject(Logger) private logger: Logger) {}

  getUser() {
    this.logger.log('Getting user...');
    return { id: 1, name: 'John Doe' };
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Resolve Services Use the ServiceProvider to resolve instances of your services.
// src/app.ts
import { ServiceProvider } from 'di-injectable';
import { UserService } from './services/userService';

const serviceProvider = new ServiceProvider();

const userService = serviceProvider.resolve<UserService>(UserService);
const user = userService.getUser();
console.log(user);
Enter fullscreen mode Exit fullscreen mode

Explanation

  • Defining Services:
    • The Logger service is a simple logger class.
    • The UserService class depends on the Logger service. The @Inject decorator is used to inject the Logger service
    • into the UserService constructor.
  • Registering Services:
    • We register the Logger service as a Singleton, meaning only one instance of Logger will be created and shared.
    • We register the UserService as a Transient by default, meaning a new instance of UserService will be created every time it is resolved.
  • Resolving Services:
    • We create a ServiceProvider instance.
    • We resolve an instance of UserService using the serviceProvider. The UserService will have the Logger instance injected into it due to the @Inject decorator.

Service Lifetimes

  • Singleton: Only one instance of the service is created and shared.
  • Transient: A new instance of the service is created every time it is requested.

API Reference

  • ServiceProvider:
    • resolve<T>(token: any): T: Resolves an instance of the service.
    • Injectable: Decorator to mark a class as injectable as register it.
    • Inject: Decorator to inject dependencies into the constructor.

Conclusion

Dependency injection is a powerful technique that improves code maintainability, testability, and flexibility. By leveraging constructor or property injection, we can create loosely coupled components that are highly reusable and easy to test.

As software engineers, embracing dependency injection in our TypeScript projects empowers us to write cleaner, more modular, and robust code. It enhances the scalability of our applications, enables efficient collaboration between team members, and simplifies the introduction of new features or changes.

Top comments (0)