Inversion of Control applied to typescript
2023-03-25 18:30:36

Inversion of Control (IoC) is a design pattern that allows for loose coupling and a more modular approach to coding. It involves inverting the traditional flow of control between the different components of a program. Instead of classes or modules directly creating or calling other modules or classes, they rely on an external “entity” (it could be a framework, module or even just a main entry point) to manage their dependencies.

In JavaScript/Typescript world, the most commonly used way for implementing IoC is Dependency Injection (DI). DI is a technique that allows a component to specify its dependencies, which are then injected into the component externally. This way, the component is decoupled from its dependencies, making it more reusable and easier to test.

Also this concept could sound familiar to whoever have heard about SOLID’s principles, there is one special Dependency Inversion Principle (DIP) which satisfies the following:

High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.

Let’s see a very easy practical example of how this principle can be applied when building API’s using Node:

First of let’s see the following example using Javascript, as we don’t have to deal with type checking the most simple way I can think of for injecting a dependency might be:

1
2
3
4
5
6
7
8
9
10
class UserService {

constructor(usersRepository) {
this.usersRepository = usersRepository;
};

async getUsers() {
return await this.usersRepository.find();
};
};

Then when testing UserService in isolation you just need to pass down an object with an implemented find property and build your expectations accordingly. e.g:

1
2
3
4
5
6
7
8
9
10
11
it('user service happy path', async () => {
const fakeUserRepository = {
find: jest.fn(() => Promise.resolve([]))
};

const userService = new UserService(fakeUserRepository);

await userService.getUsers();

expect(fakeUserRepository.find).toHaveBeenCalled();
});

But the story is somewhat different when dealing with Typescript and type checking…

1
2
3
4
5
6
7
8
9
10
11
export class UserService {
usersRepository: UserRepository;

constructor(usersRepository: UserRepository) {
this.usersRepository = usersRepository;
};

getUsers() {
return this.usersRepository.find()
};
};

Now trying to test a Typescript class as before we would see an error when trying to create an UserService instance passing down fakeUserRepository because UserService needs a full UserRepository implementation.

Then is when Typescript Interfaces come to the rescue and with a very little tweak defining UserService class, the ability to use fakes become easy again.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

export interface IUserRepository {
find: () => IUser[]
};

export class UserService {
usersRepository: IUserRepository;

constructor(usersRepository: IUserRepository) {
this.usersRepository = usersRepository
};

async getUsers() {
return await this.usersRepository.find()
};
};

Now fakeUserRepository just needs to satify the interface IUserRepository for being a valid UserService dependency.

1
2
3
4
5
6
7
8
9
10
11
12
it('user service happy path', async () => {

const fakeUserRepository = {
find: jest.fn(() => Promise.resolve([]))
}

const userService = new UserService(fakeUserRepository);

await userService.getUsers();

expect(fakeUserRepository.find).toHaveBeenCalled();
});

Here there is a fully implementation in express of what’s described here, this can be used as a playground, bear in mind that this is just a simple example of how to wrap your head around using dependency injection in typescript. Applying this concept to production services might require relying on third parties like InversifyJS for applying IoC effectively.

Happy coding!