Typescript FP Dependency Injection Is Easy!

tareksalem

tareksalem

Posted on July 28, 2023

Typescript FP Dependency Injection Is Easy!

Doing Dependency injection using a framework or library is something easy to do, there some frameworks supports dependency injection out of the box either for frontend or backend, for example Nestjs supports dependency injection for backend development, and angular supports it with the same pattern for frontend development, and there are many libraries help you to do dependency injection.
But with all these packages and libraries they support a one pattern for dependency injection which is using classes, I think there is a clear reason behind that, dependency injection was driven from OOP programming languages like Java or C# and came to typescript/javascript with a similar syntax, and we as a js/ts developers didn't notice that Javascript doesn't support only one paradigm, it supports many paradigms, it supports OOP and Functional Programming!

The Story started when I joined a company was using FP like paradigm for building all backend functionalities using functions, but the code had a very clear problem, "The Code doesn't do dependency injection"
That's why I said it uses FP like because it doesn't follow all functional programming principles, the developers where just writing functions and use their dependencies directly inside the functions.
You may ask yourself what is the problem with this pattern? lemme list some problems we faced with this pattern

  • hard to test because to test one function you need to mock all the dependencies it needs, and we were using jest so we had a lot of jest.mock("../filepath") which make things hard to do different mocks for the same dependency
  • the code has side effects because all the functions are impure functions
  • hard to change the code base because if the function depends on another function and we want to replace this function with another one, we need to update all code parts that uses it, which means alot of refactoring just to change one function signature, that's why we need to use Inversion of control principle and dependency injection pattern so we will be able to depend on interfaces and types instead of concrete implementation, so anytime we want to replace the concrete implementation with another function we can change that function only!

Dependency Injection in Javascript/typescript

Doing dependency injection in javascript/typescript can be implemented easily if you are using classes with the help of some libraries like:

  • inversify
  • nestjs
  • typedi

and other libraries, but when it comes to functional programming, there is no many libraries doing that for functions, some people thought the dependency injection is applicable only in OOP, I wanna say that's wrong, it's applicable also in functional programming, there are many ways you can do that, I suggest to you read this article on Medium%20those%20dependencies%20as%20arguments.)
It has very nice information, but for now I will give you some ways you can do the dependency injection in pure javascript without any other dependencies

One of the ways you can do that is using Higher order function, but before giving an example, lets first define what is higher order function?

In a nutshell the higher order function is a function that accepts functions as parameters and returns another function.

In nodejs all of us know the concept of callbacks, basically the function that accepts a callback is considered a higher order function

fs.readFile('input.txt', function (err, data) {
   if (err) return console.error(err);
   console.log(data.toString());
});
Enter fullscreen mode Exit fullscreen mode

as you see, the file system readFile method is a higher order function because it takes a callback as an argument and execute it.

In the same way, the Array functions like map, reduce, forEach and others are higher order functions, they accept a function to be passed as a parameter and execute them.

The other type of higher order function is the function that doesn't accept a function as an argument but it returns a function, you can think about it like a closure, which means a function scoped inside another function, and this is what we need to do dependency injection in our functions.

The concept is very easy, in OOP we have a class like this

class SomeClass {
  constructor() {}

  getUser(userId: string) {
    this.httpService.get('pathHere')
  }
}
Enter fullscreen mode Exit fullscreen mode

we have getUser function, this function depends on something called httpService and the way to pass this service to the class is to inject it inside the constructor, so we can do something like this:

class SomeClass {
  constructor(httpService: HttpService) {
    this.httpService = httpService;
  }

  getUser(userId: string) {
    this.httpService.get('pathHere')
  }
}
Enter fullscreen mode Exit fullscreen mode

In that way you can thing about the higher order function as a constructor in OOP, it knows how to return the inner function and knows what dependencies it needs so basically you can do something like this:

function getUser(httpService: HttpService) {
  return async (userId: string) => {
    const result = await httpService.get('pathHere')
  }
}
Enter fullscreen mode Exit fullscreen mode

Now you are doing higher order function, and you also do dependency injection, so anytime you want to consume getUser function you can pass the dependency it needs and it returns a function you can execute it, so for example you consume it like this:

async function main() {
  const httpService = new HttpService();
  const user = await getUser(httpService)("1234")
}
Enter fullscreen mode Exit fullscreen mode

We created an instance from the httpService and passing it to getUser higher order function and as you know, it returns another function so we pass the userId to that function.This is pretty awesome, because now you can test getUser function in an isolated way without knowing anything about httpService, so you could have unit testing like this:

describe('getUser Test Suite', () => {
   const mockedHttpService = {
     get: jest.fn()
   }
   const getUserFunc = getUser(mockedHttpService)
  it("testing getUser", async () => {
     mockedHttpService.get.mockReturnValue({ data: { userId: "1234" } })
     const user = getUserFunc("1234")
  });
});
Enter fullscreen mode Exit fullscreen mode

You can pass anything in place of the httpService, and you can control how it acts without knowing anything about the concrete implementation of it, and you don't need to use jest.mock('modulePath') to mock specific module!

Is that enough to do dependency injection in real projects?

In real projects this pattern is good but it has one downside, which is passing the dependencies from one function to another. For example suppose getUser function is called from another function, now you need to inject the needed dependencies to that function and inject them in the outer function until you reach the project entrypoint, you will have something like this:

A function that fetches the user from an http service based on the username

// getUser.ts
function getUser(httpService: HttpService) {
  return async (username: string) => {
    const result = await httpService.get('pathHere')
  }
}
Enter fullscreen mode Exit fullscreen mode

That function is called inside another function that says the user exist or not

// checkUserExists.ts
function checkUserExists(httpService: HttpService) {
  return async (username: string) => {
    const user = await getUser(httpService)(username)
    return user ? true : false;
  }
}
Enter fullscreen mode Exit fullscreen mode

and that function is used inside createNewUser function that checks the user exist or not before creating it

// createNewUser.ts
function createNewUser(httpService: HttpService) {
  return async ({username}: {username: string}) => {
    const userExists = await checkUserExists(httpService)(username) 
    (username);
    if (userExists) {
      throw new Error("Duplicate user")
    }
   // create the user here
  }
}
Enter fullscreen mode Exit fullscreen mode
function main() {
  const httpService = await new HttpService();
  await createUser(httpService)({ username: 'John' })
}
Enter fullscreen mode Exit fullscreen mode

You may observed you need to pass the dependencies from the outer level to the inner level, this is may work in small scale, but it will be very hard to manage the flow of dependencies in large scale. also you need to know if the httpService is instantiated before or not, should be instantiated as singleton or transit

What to do to manage the dependencies?

There are two ways to do that:

  • compile time
  • runtime

The compile time dependency injection is used to manage the dependencies and pass all the needed dependencies before the program run, this is commonly used in compiled languages like golang.

The runtime dependency injection is used to do the injection of the dependencies at runtime, so when the application is instantiated then the dependencies will be started to be resolved and cached somewhere and injected in the functions/classes that depends on it. This pattern requires an important something called DI container you can consider it as a place where you put all the created dependencies inside it and start pick from it to resolve the functions/classes that use it.

InjectX

InjectX is built for dependency injection in functional programming, it's designed for that purpose, it uses the higher order functions paradigm to inject the needed dependencies automatically at runtime.

InjectX has three main components:

  • di container: a pool has all the resolved dependencies
  • InjectIn: a function that used to tell InjectX to inject the needed dependencies inside the higher order function
  • Bind: a function used to bind a dependency to the container.

Install InjectX

npm i injectx
Enter fullscreen mode Exit fullscreen mode

You can also see the full documentation from here

Let's take the same example we had above. You will have a higher order function like this:

// getUser.ts
function GetUser(httpService: HttpService) {
  return async (username: string) => {
    const result = await httpService.get('pathHere')
  }
}
Enter fullscreen mode Exit fullscreen mode

as you can see I just changed getUser to GetUser because each module I will export two functions

// getUser.ts
export function GetUser({httpService}: {httpService: HttpService}) {
  return async (username: string) => {
    const result = await httpService.get('pathHere')
  }
}
export const getUser = InjectIn(GetUser)
Enter fullscreen mode Exit fullscreen mode

Let's explain what we are doing here, I export two functions here, the first one is the higher order function which expects some dependencies to be injected in and this function takes the dependencies as an object. The second exported function which is getUser is the resolved function. InjectIn basically takes the higher order function and passes the needed dependencies to it and returns the inner function of that higher order function with resolving all the needed dependencies, so you can use getUser function anywhere without the need to pass the needed dependencies

so you can have something like this

import { getUser } from './getUser'
async function main() {
  const user = await getUser("username")
}
Enter fullscreen mode Exit fullscreen mode

As you can see, no need to pass httpService dependency to the function directly because it's injected using InjectIn function. Now all you need before calling getUser is to bind the HttpService to the container, and to do that you can use this:

import { GetContainer } from 'injectx';
const httpService = new HttpService();
GetContainer("default").Bind(httpService);
Enter fullscreen mode Exit fullscreen mode

We talked about the container before, it's a place where you put the dependencies inside it and start picking what you need from it right? InjectX allows you to create multiple containers, why multiple containers and not only one container?
Suppose you have multiple modules like the following:

  • orders module
  • catalog module
  • auth module

and you want to separate between the dependencies of each module so your dependencies will be organized, then you can create multiple containers, each container is related to one module and you put that module dependencies inside the related container.
To do that you use GetContainer function which is a function exported from injectX, it asks injectX to get a container with specific name, if the container does not exist, then it will create a new one with the specified name
Then you can access another method called Bind which means you are telling inectX I want to append a dependency to that container so it will be available to the higher order functions.

If you are interested to know more about dependency injection in functional programming you can comment bellow and wait for the next part.

Thank you for reading

💖 💪 🙅 🚩
tareksalem
tareksalem

Posted on July 28, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related