Functional design: TDD in TypeScript (aka abusing `declare`)
Tags: functional, typescript
Published: 2019-03-18 08:48:57

---
title: Functional design: TDD in TypeScript (aka abusing `declare`)
published: true
description: 
tags: functional, typescript
series: Functional design
---

**Type** driven development (TDD) is a technique used to split a problem into a set of smaller problems, letting the type checker suggest the concrete implementation, or at least helping us getting there. Here's a practical example.

Say for instance that we like to reimplement the function `Promise.all`, we'll name it `sequence`. Let's start with its signature

```ts
// TODO
declare function sequence<T>(promises: Array<Promise<T>>): Promise<Array<T>>
```

Notice that a `declare`d function, even if not implemented yet, is immediately available and can concur to type check the rest of our code. If we are running a build system though, we'll get an error because there's no such function at runtime, it only exists in the "world of types" for now.

However, the goal of this technique is **working in the editor for as long as possible without the need to run our code**. We'll rely entirely on the type checker making sure it doesn't raise any errors as we go through.

Back to our problem, we need to transform (or "reduce") an array of values of type `A` to a value of type `B`.

The transformation we're looking for is likely `reduce`, which in fact has the following signature

```ts
declare function reduce<A, B>(this: Array<A>, f: (acc: B, x: A) => B, init: B): B
```

We don't know how to implement neither `f` nor `init` yet, but we can skip the concrete implementation as we did before. For now it's enough to simply `declare` the missing bits and replace the type parameters `A` and `B` with the corresponding types we're working with:

```ts
A = Promise<T>

B = Promise<Array<T>>
```

This is what we get

```ts
// TODO
declare function pushPromise<T>(acc: Promise<Array<T>>, x: Promise<T>): Promise<Array<T>>

// TODO
function sequence<T>(promises: Array<Promise<T>>): Promise<Array<T>> {
  declare const init: Promise<Array<T>> // TypeScript error
  return promises.reduce(pushPromise, init)
}
```

Alas `declare` can't be used inside a function body so we need a temporary workaround

```ts
declare const TODO: any

// TODO
declare function pushPromise<T>(acc: Promise<Array<T>>, x: Promise<T>): Promise<Array<T>>

// partially implemented
function sequence<T>(promises: Array<Promise<T>>): Promise<Array<T>> {
  const init: Promise<Array<T>> = TODO
  return promises.reduce(pushPromise, init)
}
```

Now `init` is straightforward, we can put an empty array into the Promise "container"

```ts
const init: Promise<Array<T>> = Promise.resolve([])
```

Implementing `pushPromise` is more complicated, we know how to concat a value of type `Array<T>` with a value of type `T` to get another value of type `Array<T>`, let's name it `push`

```ts
declare function push<T>(x: Array<T>, y: T): Array<T>
```

but how do we concat `acc` and `x` in `pushPromise` given that they are both promises?

What we'd like to have is a procedure, let's name it `liftA2`, which can "lift" the function `push` producing a new function which can work on the values "inside" the promises. Again we just `declare` the expected result without any implementation

```ts
// TODO
declare function liftA2<A, B, C>(
  f: (a: A, b: B) => C
): (fa: Promise<A>, fb: Promise<B>) => Promise<C>

// TODO
declare function push<T>(x: Array<T>, y: T): Array<T>

// implemented
function pushPromise<T>(acc: Promise<Array<T>>, x: Promise<T>): Promise<Array<T>> {
  return liftA2<Array<T>, T, Array<T>>(push)(acc, x)
}

// implemented
function sequence<T>(promises: Array<Promise<T>>): Promise<Array<T>> {
  const init: Promise<Array<T>> = Promise.resolve([])
  return promises.reduce(pushPromise, init)
}
```

Now we only need to implement `liftA2` and `push`

```ts
function liftA2<A, B, C>(
  f: (a: A, b: B) => C
): (fa: Promise<A>, fb: Promise<B>) => Promise<C> {
  return (a, b) => a.then(aa => b.then(bb => f(aa, bb)))
}

function push<T>(x: Array<T>, y: T): Array<T> {
  return x.concat([y])
}

function pushPromise<T>(acc: Promise<Array<T>>, x: Promise<T>): Promise<Array<T>> {
  return liftA2<Array<T>, T, Array<T>>(push)(acc, x)
}

function sequence<T>(promises: Array<Promise<T>>): Promise<Array<T>> {
  const init: Promise<Array<T>> = Promise.resolve([])
  return promises.reduce(pushPromise, init)
}
```

Let's try it out

```ts
sequence([]).then(x => console.log(x)) // []
sequence([Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)]).then(x =>
  console.log(x)
) // [1, 2, 3]
```

As you can see a strong type system can not only prevent errors, but also guide you and provide feedback in your design process.