home  articles  notes  projects  github  twitter  email

Purely functional dependency injection in TypeScript

swans

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> {
    client.query(query, params)
      .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:

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.

Antti Holvikari @antti