Functional Dependency Injection In Typescript
Write code that is easy to unit-test
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 a dependency, otherwise it’s just a part of the function’s logic.
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 you having to 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 with a code that doesn’t use DI.
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:
votewhich has two possible values upvote and downvote.
The function needs to do some validation before it accepts the vote, it checks for the following:
- The issue is still open for voting.
- The user has not already voted on the issue.
- 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:
queryGetIssueData: gets the issue’s data from our database and returns it.
queryHaveUserVoted: checks if a user has voted on a particular issue, it’s a promise that when resolved returns a boolean value.
checkUserIsEligibleToVote: checks if the user is eligible to vote on the issue.
queryInsertVote: inserts the vote into the database.
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
Here are a couple of downsides to the way
voteOnIssue is structured and this approach to unit-testing:
voteOnIssueis 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.
- 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
queryGetIssueDatafor 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.
voteOnIssuerequires us to use spies which entails a cognitive overhead for the junior developer that has just joined your team.
- 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:
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
Here are the benefits of our new implementation of
voteOnIssue and the implications of that on our unit-tests:
- We easily hand-crafted our stubs without having to use spies, just simple functions. Who doesn’t know how to create them?
voteOnIssueis 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).
- 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.
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.
- 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 can follow me on
Last updated: 2022–05–15