Functional Dependency Injection In Typescript

Write code that is easy to unit-test

Hugo Nteifeh
6 min readMay 1, 2022
Photo by Robert Katzki on Unsplash

For discussion, check this Reddit thread.

What is Dependency Injection?

Dependency Injection (DI) is the separation of the logic of a unit of code from its dependencies. Functional dependency injection (FDI) is a way of modeling and implementing DI using an FP approach. It simply denotes modeling the dependencies of a function as parameters and passing(injecting) those dependencies as arguments.

But What Is A Dependency Really?

“Dependency” is a somewhat vague term, which can be the most confusing part about DI. I mean, what the hell is a dependency? Is a logger instance in the body of a function a dependency? Maybe the database query used to fetch the user’s data that is crucial to the function’s output? And what about the utility function imported from some library and used in the body of a function?.

Loggers and DB-queries can always be considered as dependencies for any function. Utility functions? well, is the utility function pure (given the same input it always returns the same output on every call AND has no side effects)? if yes then it’s not a dependency, otherwise it is.

Here are a couple of tips to spot a dependency:

  • If you need to use something like Jest’s “spy” method to fake a function call inside the function you’re testing, you’re looking at a dependency.
  • If something your function depends on behaves differently or has different configurations between different environments, sign it up for the dependency party.
  • If something that your function depends on has side effects; it’s a dependency.

Why Does Dependency Injection Matter?

A function that is structured in a DI-manner is dependency transparent meaning that it explicitly defines the components that are dependencies without having you––The caller––dig into the implementation of that function. This leads to a high-level understanding of what side effects a function might be performing, and more importantly, it makes unit testing a straightforward task.

To understand these benefits, we’ll start off with a code that doesn’t use DI, and then we’ll counter that with one that does use it.

Let’s imagine that we have a voting module for voting on issues. Our module exposes a function called voteOnIssue . The function takes as input:

  1. a userId
  2. An issueId
  3. A vote which has two possible values upvote and downvote.

The function needs to do some validation before it accepts the vote, it checks for the following:

  1. The issue is still open for voting.
  2. The user has not already voted on the issue.
  3. The user is eligible to vote on the issue. For example, voters might be required to be at least 18.

Here’s an initial implementation of the function:

This function has the following dependencies:

  1. queryGetIssueData: gets the issue’s data from our database and returns it.
  2. queryHaveUserVoted: checks if a user has voted on a particular issue, it’s a promise that when resolved returns a boolean value.
  3. checkUserIsEligibleToVote: checks if the user is eligible to vote on the issue.
  4. queryInsertVote: inserts the vote into the database.
  5. logger: a logger.

Now, how do we go about unit-testing this function? obviously voteOnIssue is an impure function with side effects, so we need to create test doubles for its dependencies in our tests.

Let’s create a test file to test voteOnIssue:

Here are a couple of downsides to the way voteOnIssue is structured and this approach to unit-testing:

  1. voteOnIssue is not dependency transparent, and anyone writing tests for this function has to dig into its implementation to figure out its dependencies and create the necessary test doubles, which is time-consuming.
  2. The test is tightly coupled with the implementation of the function. For example, let’s say that we decided to switch out a deprecated dependency queryGetIssueData for a new one called queryGetIssueById, next time you run the test it will fail since it’s not spying on the correct dependency anymore. Jest won’t be able to rescue you here because it doesn’t know about the implementation of your function.
  3. Testing voteOnIssue requires us to use spies which entails a cognitive overhead for the junior developer that has just joined your team.
  4. Things become a bit more complex when the function you’re testing and any of its dependencies reside in the same module. Take a look at this on Stackoverflow. Again, think of the implication of this on junior developers.

Implementing DI Using High Order Functions

Now we’re going to reimplement voteOnIssue so that it uses FDI (Functional Dependency Injection):

voteOnIssue is now a HOF (High-order function) that first takes its dependencies and returns a new function that takes the actual input from the domain perspective: userId, issueId, and vote.

Unit testing Our DI-code

Now it’s time to reap the benefits. Let’s create a test file(I’m using Jest here) and test our voteOnIssue function:

Here are the benefits of our new implementation of voteOnIssue and the implications of that on our unit-tests:

  1. We easily hand-crafted our stubs without having to use spies, just simple functions. Who doesn’t know how to create them?
  2. Since voteOnIssue is now dependency transparent and we’re passing these dependencies as arguments, we won’t run into the issues that we would run into when we use spies to test functions used as a dependency (like in the case of swapping out a dependency with another one).
  3. This model is quite simple. No complex scenarios like how do we deal with spying on a function that resides in the same module as the function we’re testing.

For discussion, check this Reddit thread.

A Two-Layer Module Structure

I’m quite certain that at some point while demonstrating how we can use high order functions to implement DI, you were thinking something along the lines of “that’s neat, but I don’t want to keep passing around my dependencies every single time I use this function.”. And I wholeheartedly agree with you; we’re introducing noise in the codebase as the consumers of those functions are not interested in knowing about those dependencies. They just want to give it the input and get the output.

So how do we get around this?

The solution is to introduce a two-layer structure to your modules. One layer contains the uninjected code, and the other contains the code injected with dependencies. You use the uninjected layer (I usually call it logic) in your tests, and you use the injected one in your application (I typically call it index.ts).

Here’s an implementation that follows the structure for the queries module:

As you can see, in queries/index.ts we’re importing the functions that we want to use in the rest of our application from the uninjected layer queries/logic.ts injecting them with their dependencies and then re-exporting them.

Wrap up

  • Dependency Injection is not exclusive to OOP and classes; it can be easily implemented in FP using HOFs.
  • DI-structured functions are dependency transparent. A function is dependency-transparent when it explicitly defines its dependencies.
  • Adding a two-layer module structure can smooth out the verbosity of passing around dependencies.
  • Structuring functions in the way the article suggests makes unit-testing a straightforward task.
  • There are better ways than using plain promises and throwing errors to model async operations, good candidates for this would be Task/AsyncEither, but I’ll leave that to another article.
  • You could use the ReaderMonad to handle dependency injection, but I find HOFs to be doing a good job without introducing the concept of Monads.

Last updated: 2022–05–15

--

--