DEV Community

HidetoshiYanagisawa
HidetoshiYanagisawa

Posted on

Demystifying Dependency Injection in TypeScript: Comprehensive Examples and Benefits

Hello, I am a frontend engineer who is an avid TypeScript user. Today, we are going to take a deep dive into Dependency Injection in TypeScript. I will provide a comprehensive guide with rich examples that even beginners will find easy to grasp.

What is Dependency Injection?

Dependency Injection is a design pattern in software engineering, a technique to reduce dependencies among classes. The idea is simple: Instead of a class creating instances of other classes it depends on, these instances are provided to the class from an external source.

Dependency Injection in Action

Let's illustrate Dependency Injection with a basic example in TypeScript:

class MailService {
    sendEmail(message: string, recipient: string): void {
        // logic to send email
    }
}

class UserService {
    private mailService: MailService;

    constructor(mailService: MailService) {
        this.mailService = mailService;
    }

    register(name: string, email: string) {
        // logic to register user
        this.mailService.sendEmail('User registered', email);
    }
}
Enter fullscreen mode Exit fullscreen mode

The UserService class doesn't create an instance of MailService. Instead, the UserService constructor accepts an instance of MailService and uses it. Consequently, the UserService class is decoupled from the concrete implementation of MailService and can accept any implementation of MailService. This is the core idea of Dependency Injection.

How about Testing?

So, why is this design pattern useful? To illustrate, let's consider an example of testing before and after using Dependency Injection.

Without Dependency Injection

First, consider an example without using Dependency Injection:

class MailService {
    sendEmail(message: string, recipient: string): void {
        // logic to send email
    }
}

class UserService {
    private mailService: MailService;

    constructor() {
        this.mailService = new MailService();
    }

    register(name: string, email: string) {
        // logic to register user
        this.mailService.sendEmail('User registered', email);
    }
}
Enter fullscreen mode Exit fullscreen mode

Testing this might involve sending actual emails. To avoid this, you would need to mock or override the MailService class, which could be difficult because the classes are closely coupled.

With Dependency Injection

In contrast, consider testing with Dependency Injection:

class MailService {
    sendEmail(message: string, recipient: string): void {
        // logic to send email
    }
}

class UserService {
    private mailService: MailService;

    constructor(mailService: MailService) {
        this.mailService = mailService;
    }

    register(name: string, email: string) {
        // logic to register user
        this.mailService.sendEmail('User registered', email);
    }
}
Enter fullscreen mode Exit fullscreen mode

Because the dependency is injected, you can use a mock MailService in your tests:

class MockMailService {
    sendEmail(message: string, recipient: string): void {
        console.log('Mock email sent');
    }
}

const userService = new UserService(new MockMailService());

userService.register('Test user', 'test@test.com');
// Output: Mock email sent
Enter fullscreen mode Exit fullscreen mode

With Dependency Injection, it's easy to replace real dependencies with mocks or stubs in a testing environment. This facilitates writing unit tests and improves the overall quality of the software.

Why is Dependency Injection Important?

Dependency Injection offers several benefits:

  1. Ease of testing: As demonstrated, Dependency Injection makes testing easier. By replacing real dependencies with mocks during testing, unit testing becomes straightforward.

  2. Decoupling of classes: Dependency Injection reduces coupling of classes from their concrete implementations. This improves code reusability and facilitates adding new or changing existing features.

  3. Software flexibility: Dependency Injection allows classes to depend on interfaces, not specific implementations. This means that adding new implementations doesn't require changes to existing code, enhancing overall software flexibility.

Conclusion

Dependency Injection is a potent technique to manage dependencies between classes and improve ease of testing. Its implementation in TypeScript is fairly intuitive, where dependencies that a class requires are injected via the constructor. This decoupling improves code reusability and overall software quality.

Even if you're hearing about it for the first time or are yet to get comfortable with Dependency Injection, consider the examples above and try employing this technique. You'll notice improvements in code reusability, flexibility, and ease of testing.

If you found this useful, do leave a like and drop a comment if you have any topics you want to delve deeper into.

Top comments (0)