Dependency Injection in TypeScript: A Simple Guide for Beginners

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: What is Dependency Injection and Why Do You Need It?

In the world of software development, we often encounter situations where one class (or module) depends on another. For example, a UserService class might use a DatabaseService class to save data. The traditional approach is to create an instance of DatabaseService directly inside UserService. This leads to tight coupling of code, making it difficult to test, maintain, and extend.

Dependency Injection (DI) is a design pattern that solves this problem. Instead of creating dependencies inside a class, they are "injected" from the outside. This implements the Inversion of Control (IoC) principle: the class does not manage its dependencies; external code (a container or the caller) provides them.

In this article, we will break down how to implement Dependency Injection in TypeScript with simple examples. You will learn why DI is considered a good practice and how it helps write clean, testable code.



The Problem: Tight Coupling Without DI

Let's start with an example that shows how not to write code. Imagine we have an application that sends notifications to users.

// Without Dependency Injectionclass EmailService {    sendEmail(to: string, message: string): void {        console.log(`Sending email to ${to}: ${message}`);        // ... actual sending logic    }}

class NotificationService { private emailService: EmailService;

constructor() { // Problem: tight coupling this.emailService = new EmailService(); }

notifyUser(userEmail: string, message: string): void { this.emailService.sendEmail(userEmail, message); }}

// Usageconst notifier = new NotificationService();notifier.notifyUser('user@example.com', 'Hello!');

In this code, NotificationService is tightly coupled to EmailService. What if we want to send SMS or push notifications? We would have to modify the NotificationService code. Additionally, testing NotificationService is difficult — we cannot replace the real EmailService with a mock object.



The Solution: Injecting Dependencies Through the Constructor

The simplest and most common way to implement DI is to pass dependencies through the constructor. This makes the code flexible and easily testable.

First, let's define an interface for our notification service. This is a key step: we program at the level of abstractions, not concrete implementations.

// Define an interface for any notification serviceinterface INotificationService {    send(to: string, message: string): void;}

// Implementation for Emailclass EmailService implements INotificationService { send(to: string, message: string): void { console.log(`[EMAIL] Sent to ${to}: ${message}`); }}

// Implementation for SMSclass SMSService implements INotificationService { send(to: string, message: string): void { console.log(`[SMS] Sent to ${to}: ${message}`); }}

// Class that uses DIclass UserNotifier { private notifier: INotificationService;

// Dependency is injected through the constructor constructor(notifier: INotificationService) { this.notifier = notifier; }

notifyUser(userContact: string, message: string): void { this.notifier.send(userContact, message); }}

// Usageconst emailNotifier = new UserNotifier(new EmailService());emailNotifier.notifyUser('alice@example.com', 'Your order is ready!');

const smsNotifier = new UserNotifier(new SMSService());smsNotifier.notifyUser('+7-999-123-45-67', 'Confirmation code: 1234');

Now UserNotifier does not know which specific service

Blogs

Book Recommendations