Purely functional dependency injection in TypeScript
A very common pattern I see in web applications on the server side is a database connection module that exposes a bunch of functions. These functions run queries to the database and may call other functions defined in the module to abstract some of the common database operations. Let’s look at one possible solution.
function query(client: pg.Client, sql: Query, params: Param[]): Task<pg.QueryResult> {
return client.query(sql, params)
}
function fetchUser(client: pg.Client, id: number): Task<User> {
return query(client, "SELECT first_name, last_name from users where id = ?", [id])
.map(rows => rows[0])
}
This is the most obvious solution. We just pass around the client to
every function. But that is also the problem with this approach.
The database functions quickly become infectious: every function
that uses one of those needs to also receive the pg.Client
as argument.
This is a problem we want to solve with dependency injection. In object oriented programming we usually create a class and inject the DB client to it. The functions become methods in that class and have access to the client.
class Database {
constructor(client: pg.Client) { }
query(sql: Query, params: Param[]): Task<pg.QueryResult> {
return this.client.query(sql, params)
}
fetchUser<T>(id: number): Task<User> {
return this.query("SELECT first_name, last_name from users where id = ?", [id])
.map(rows => rows[0])
} }
This somewhat solves the problem and we no longer need to pass the
pg.Client
around. However, we then need to pass that class
around we just created. We also need to write code in a more object
oriented style: we can no longer just pass functions around because we
have an object instead the plain functions. And I like functions, they
are easy to work with.
How can we keep the simplicity of functions without having to pass that client around?
The essence of the database layer
The trick is to identify the essence of the database abstraction. Looking at the functions we have, what are the parts that are common to both?
function query(client: pg.Client, sql: Query, params: Param[]): Task<pg.QueryResultRow[]> {
}
function fetchUser(client: pg.Client, id: number): Task<User> {
}
Both of them take the raw database client as input and return a
Task
. Ok, so extracting the common parts out we are left
with a function type:
function query<A>(client: pg.Client): Task<A>
Note the introduction of a type variable A
for the
return value inside the Task
.
So, with the common parts extracted out into a type, what do we do with it?
What we can do is, instead of returning a Task
from our fetchUser
function, we return the query function
itself.
function fetchUser(id: number): (client: pg.Client) => Task<User> {
// Returning the `query` instead of calling it!
return function query(client: pg.Client): Task<User> {
return client.query("SELECT first_name, last_name from users where id = ?", [id])
.map(result => result.rows[0])
} }
The return type now has the same type as the query
function had! In fact, let’s extract that type out so it becomes a bit
clearer, and let’s call it DatabaseFn
because it represents
our database abstraction.
type DatabaseFn<A> = (client: pg.Client) => Task<A>
function fetchUser(id: number): DatabaseFn<User> {
return function (client: pg.Client): Task<User> {
return client.query("SELECT first_name, last_name from users where id = ?", [id])
.map(result => result.rows[0])
} }
So now our fetchUser
itself returns a function. This
also means that calling the fetchUser
function doesn’t
require us to have the client available.
To actually run our query (really just to give us a
Task
) we must call the database function we returned:
fetchUser(42)(client)
What all this has now given us is a pure function which we can just
import from anywhere and call without needing to provide the
client
value. We can rely on the fact that the
client
will be passed in later at the top level of the call
stack when we actually want to run the query.
More complex domain logic
Now, what if we want to further abstract things and call
fetchUser
from another DB function? Do we then need to have
the client available? No. We can just keep returning those database
functions.
// Fetch one row, fail if no rows were found
function queryOne(sql: Query, params: Param[]): DatabaseFn<QueryResultRow> {
return (client: pg.Client): Task<QueryResultRow> {
.query(query, params)
client.chain(rows => {
return rows.length > 0
? Task.of(rows[0])
: Task.rejected(new Error('Query returned no rows'))
})
}
}
function authenticate(username: string, password: string): DatabaseFn {
return function (client: pg.Client): Task<User> {
const query = "SELECT id from users where username = ? and password = ?"
// `queryOne` returns a `DatabaseFn` so we need to pass it a `client`
// in order to give us a `Task` so we can `chain`.
return queryOne(query, [username, password])(client)
.chain(id => {
// Same here, need to unwrap our `DatabaseFn`.
return fetchUser(id)(client)
})
} }
We are still returning a DatabaseFn
. Note that
chain
is a method from Task
, and since we are
inside a Task
we must also return a Task
. Each
time we call a query function we need to unwrap the
DatabaseFn
to be able to chain with another call.
All that DatabaseFn
unwrapping creates too much
boilerplate and is error prone to write. It doesn’t compose well. Can we
do better?
Make it a datatype
In order to combine our database functions more easily we need some kind of helper functions or something to make it simpler. There are several solutions to this, but I’m proposing a solution here that uses classes.
Instead of passing around the raw DatabaseFn
function
around, let’s wrap it in a class.
class DatabaseFn<A> {
constructor(readonly fn: (client: pg.Client) => Task<A>) {}
}
We can now add methods to this class to work with the wrapped
function. Let’s add chain
which allows call chaining like
we did in authenticate
.
class DatabaseFn<A> {
constructor(readonly fn: (client: pg.Client) => Task<A>) {}
chain<B>(f: (a: A => DatabaseFn<B>)): DatabaseFn<B> {
return new DatabaseFn((client: pg.Client) => {
this.fn(client).chain(v => f(v)(client))
})
} }
This is nice but we don’t have any way of running queries, we can
only chain them! Let’s write a query
combinator for
this.
function query(sql: Query, params: Param[]): DatabaseFn<QueryResultRow[]> {
return new DatabaseFn((client: pg.Client) => {
return client.query(sql, params)
}) }
Now we can rewrite queryOne
and
authenticate
with this.
// Fetch one row, fail if no rows were found
function queryOne(sql: Query, params: Param[]): DatabaseFn<QueryResultRow> {
return query(query, params)
.chain(rows => {
return rows.length > 0
? DatabaseFn.of(rows[0])
: DatabaseFn.throw(new Error('Query returned no rows'))
})
}
function authenticate(username: string, password: string): DatabaseFn<User> {
return queryOne("SELECT id from users where username = ? and password = ?", [username, password])
.chain(row => fetchUser(row.id))
}
Much nicer! (The implementation of DatabaseFn.of
and
DatabaseFn.throw
are left as an exercise for the reader
:)
We can now write pure functions to run queries against a database and
we are not required to pass in the client
argument around.
What’s more, by wrapping the database function into it’s own datatype we
can attach methods to it for easier chaining. Contrasting this to the
object oriented approach to dependency injection, we don’t pass in the
client to any object beforehand. Instead, we combine our database
interactions using chain
and then at the end pass in the
client to run the program.
The reader pattern
When you see that you have many functions that receive the same input, like a resource of some kind, I propose the following:
- Identify the common input to your functions.
- Identify the return type and make it polymorphic by adding a type variable.
- Wrap the function you get out of these into a class.
- Add helper methods to that class:
map
andchain
are good candidates. - Write your domain logic using pure functions that return instances of your class.
The “pattern” I’m describing here can itself be generalized into a
datatype. In Haskell and PureScript this is called the reader monad
transformer, ReaderT
.
While it is possible to implement ReaderT
in Typescript and
Javascript, I think this simple pattern will get you a long way. There
is an implementation of ReaderT
for TypeScript in fp-ts
if you want to dig deeper.