Dependency Injection in TypeScript: A Complete Guide with Examples

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

What is Dependency Injection in TypeScript and why is it needed?



Dependency Injection (DI) is a design pattern that implements the Inversion of Control (IoC) principle. Instead of an object creating its own dependencies, they are "injected" from the outside. In TypeScript, this pattern is especially useful due to strong typing and decorator capabilities.



Imagine you are building an online store application. You have a class OrderService that needs to send notifications. Without DI, it would be tightly coupled to a specific sending method (e.g., Email). With DI, you can easily swap Email for SMS or Push notifications without changing the OrderService code.



Why should you use DI in TypeScript?

  • Testability: Easily replace real services with mock objects in unit tests.
  • Flexibility: The system becomes modular; dependencies can be changed without modifying client code.
  • Simplified maintenance: Each class is responsible only for its own business logic, not for creating other objects.
  • Type safety: TypeScript checks types at compile time, reducing the number of runtime errors.


Basic implementation of DI in TypeScript without frameworks



Let's start with a simple example. We'll create an interface for a notification service and two of its implementations.



// Define the interfaceinterface INotificationService {    send(message: string): void;}

// Implementation for Emailclass EmailNotificationService implements INotificationService { send(message: string): void { console.log(`Sending Email: ${message}`); }}

// Implementation for SMSclass SMSNotificationService implements INotificationService { send(message: string): void { console.log(`Sending SMS: ${message}`); }}

// Class that uses DIclass OrderService { constructor(private notificationService: INotificationService) {}

processOrder(orderId: number): void { // Order processing logic this.notificationService.send(`Order #${orderId} processed`); }}

// Usageconst emailService = new EmailNotificationService();const orderService = new OrderService(emailService);orderService.processOrder(123); // Sending Email: Order #123 processed


In this example, OrderService does not know which specific notification service is being used. It simply calls the send method through the interface.



Advanced DI: Creating a simple IoC container



For large projects, manual dependency injection becomes cumbersome. This is where an IoC container comes to the rescue. Let's look at how to implement one using TypeScript decorators.



Step 1: Enable decorator support in tsconfig.json

{    "compilerOptions": {        "experimentalDecorators": true,        "emitDecoratorMetadata": true    }}


Step 2: Create a DI container

// di-container.tstype Constructor<T> = new (...args: any[]) => T;

class DIContainer { private services = new Map<string, any>();

register<T>(token: string, implementation: Constructor<T>): void { this.services.set(token, implementation); }

resolve<T>(token: string): T { const Implementation = this.services.get(token); if (!Implementation) { throw new Error(`Service "${token}" is not registered`); }

// Get constructor parameter types via reflection const paramTypes: Constructor<any>[] = Reflect.getMetadata('design:paramtypes', Implementation) || [];

// Recursively resolve dependencies const dependencies = param

Blogs

Book Recommendations