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

Dependency Injection in TypeScript: For Beginners

\n\n

In the world of software development, the term Dependency Injection (DI) is often encountered. For a beginner TypeScript developer, this may sound intimidating, but in reality it is a simple and powerful pattern that makes your code cleaner, more testable, and easier to maintain. In this article, we will break down what DI is, why it is needed, and how to apply it in TypeScript in practice.

\n\n

What is Dependency Injection?

\n\n

Imagine you are assembling a computer. Instead of soldering the processor directly onto the motherboard at the factory, you leave a slot into which you can insert any compatible processor. Dependency Injection works on the same principle: instead of a class creating its own dependencies (e.g., a database or HTTP client), it receives them from the outside.

\n\n

In simple terms, DI is when an object does not create its "helpers" inside itself, but receives them ready-made through a constructor or method.

\n\n

The Problem Without DI: Tight Coupling

\n\n

Let's look at an example often found in real projects:

\n\n
class UserService {\n    private database: MySQLDatabase;\n\n    constructor() {\n        this.database = new MySQLDatabase(); // Tight coupling!\n    }\n\n    getUser(id: number) {\n        return this.database.findUserById(id);\n    }\n}
\n\n

Here, UserService is tightly coupled to MySQLDatabase. If you want to switch to PostgreSQL or write unit tests with a fake database, you will have to rewrite the entire class. This violates the Open-Closed principle from SOLID.

\n\n

The Solution with DI: Flexibility and Testability

\n\n

Now let's rewrite the same class using DI:

\n\n
interface Database {\n    findUserById(id: number): User | null;\n}\n\nclass UserService {\n    constructor(private database: Database) {} // Dependency is passed from outside\n\n    getUser(id: number) {\n        return this.database.findUserById(id);\n    }\n}\n\n// Usage\nconst mySQL = new MySQLDatabase();\nconst service = new UserService(mySQL); // Injecting the dependency
\n\n

Now UserService doesn't know which database it is working with. You can easily replace MySQLDatabase with PostgreSQLDatabase or MockDatabase for testing.

\n\n

Main Ways to Inject Dependencies

\n\n

In TypeScript, there are three main ways to inject dependencies. Let's look at each of them.

\n\n

1. Constructor Injection

\n\n

This is the most common and recommended method. Dependencies are passed through the class constructor.

\n\n
class Logger {\n    log(message: string) {\n        console.log(`[LOG]: ${message}`);\n    }\n}\n\nclass OrderService {\n    constructor(private logger: Logger) {}\n\n    createOrder(data: any) {\n        this.logger.log('Order created');\n        // order creation logic\n    }\n}\n\nconst logger = new Logger();\nconst orderService = new OrderService(logger);
\n\n

2. Property Injection

\n\n

The dependency is set directly into a property of the object after its creation. It is used less frequently as it can lead to incomplete object initialization.

\n\n
class EmailService {\n    sendEmail(to: string, message: string) {\n        console.log(`Email sent to ${to}: ${message}`);\n    }\n}\n\nclass NotificationService {\n    emailService!: EmailService; // Type assertion\n\n    notifyUser(user: string) {\n        this.emailService.sendEmail(user, 'Hello!');\n    }\n}\n\nconst notification = new NotificationService();\nnotification.emailService = new EmailService(); // Setting the property\nnotification.notifyUser('user@example.com');
\n\n

3. Method Injection

\n\

Blogs

Book Recommendations