Interfaces for scaling and testing JavaScript
Tags: javascript, typescript, flowtype, flow
Published: 2019-06-29 22:37:02

---
title: Interfaces for scaling and testing JavaScript
published: true
description: Why an old and often-misunderstood OOP tool is the key to taming JavaScript complexity
tags: javascript,typescript,flowtype,flow
---

I'VE recently been working with Flow, the JavaScript typechecker, and come to confirm my suspicions that _interfaces_ are the 'killer app' of both Flow and TypeScript. They function in a remarkably similar style in both typecheckers–you'd almost think that their designers collaborated to come up with their functionality. And most importantly because of that functionality, they preserve a lot of JavasScript's 'dynamic' look-and-feel while backing it up with solid type safety.

Here's an example interface (in Flow, but as I said, TypeScript is remarkably similar):

```javascript
export interface WeatherService {
  getForecast(
    city: string,
    numDays: number,
    tempUnit?: TempUnit,
  ): Promise<Forecast[]>,
}

/** Celsius or Fahrenheit */
export type TempUnit = 'C' | 'F'

export interface Forecast {
  city: string,
  day: {year: number, month: number, day: number},
  tempUnit: TempUnit,
  highTemp: number,
  lowTemp: number,
}
```

## A quick recap

Interfaces in Flow and TypeScript describe the _shapes_ of JavaScript objects. Remember that in JavaScript, a lot of different things are objects–starting from record-style object literals like `{a: 1, b: true}` to class instances like `new Date()`, to arrays like `[1, 2]`, and even functions like `x => x + 1`. Interfaces can describe them all, from standard attributes like `Forecast#city` to methods like `WeatherService#getForecast`, and more.

What's more, interfaces can describe objects _after the fact,_ that is, after the objects/classes/etc. have already been defined. This makes them a lot less clunky than the traditional interfaces you may be used to from object-oriented languages where classes need to explicitly _implement_ interfaces (which they can in TS/Flow too, but don't need to).

## Interface conformance

Even better: interfaces only demand that conforming objects contain _at least_ the listed members. That is, they don't care if they contain _more_ members. For example, objects of the following class will conform to `WeatherService`:

```javascript
/** `baseUri` must not end with a slash */
export default function liveWeatherService(
  baseUri: string,
): WeatherService {
  return new LiveWeatherService(baseUri)
}

class LiveWeatherService {
  baseUri: string

  constructor(baseUri: string) {
    this.baseUri = baseUri
  }

  async getForecast(
    city: string,
    numDays: number,
    tempUnit?: TempUnit = 'C',
  ): Promise<Forecast[]> {
    const response = await fetch(
      `${this.baseUri}/forecast?city=${city}&numDays=${numDays}&tempUnit=${tempUnit}`,
    )
    const json = await response.json()

    // Assume a function to decode raw JSON to `Forecast` type
    return decodeForecasts(json)
  }
}
```

The exported function pulls off a neat little trick–it creates a new `LiveWeatherService` instance and upcasts it to `WeatherService`, which succeeds because the object conforms to the interface (even without explicitly declaring it so). This also enforces that the `LiveWeatherService`'s `baseUri` instance member is hidden (i.e. private), despite Flow not actually supporting class private members. (This works the same way in TypeScript, but typically people use private class members there.)

In fact, the `LiveWeatherService` class itself is hidden–users have no way to access and instantiate it directly. We provide a simple function to instantiate a `WeatherService` object to decouple interface and implementation.

## Abstract data types

The `WeatherService` interface hides _everything_ about the underlying object except that we can do one thing with it, that is call its `getForecast` method. This is really the essence of abstract data types–types that are defined solely in terms of what you can do with them, not exposing their internal shape.

Flow/TS pull off a neat twist on that though, in a way that plays to JavaScript's strengths: they let interfaces contain members that are simple JavaScript object properties, meaning that they also let you expose exactly their internal shape. This is invaluable in a language like JavaScript, where objects can be created and passed around in such a lightweight way, and come in handy for many different uses, from writing query DSLs to passing in optional parameters to functions.

Coming back to abstract data types (ADTs) for a second–they are, of course, one of the [big breakthroughs](https://youtu.be/w2PmXJL8_WM) in our understanding of how to program at large scale. Using types, modules, or classes that hide implementation details from each other, we're able to achieve that sought-after goal of programming: decoupling, i.e. [cutting dependencies](https://www.tedinski.com/2018/07/31/interfaces-cutting-dependencies.html).

## Testability

One of the places where this is most apparent is when testing your code. Automated tests are the first consumer of every well-designed codebase, and especially unit tests serve as a canary of how modular the code is. If it's not modular enough that its units can be tested in isolation, chances are it's not flexible and robust in the face of inevitable change.

The key to unit tests is the 'unit' part–that is, testing _one_ thing, in isolation. And as I mentioned, interfaces allow you to cut dependencies and thus isolate pieces of code from each other.

For example, suppose you want to test some piece of code that uses the above `WeatherService`. This code may be simple or complex. Let's assume for now it's some logic to return a message depending on today's high temperature:

```javascript
export async function weatherMessage(
  weatherService: WeatherService,
): Promise<string> {
  const [{highTemp}] = await weatherService.getForecast(
    /* city = */ 'Toronto',
    /* numDays = */ 1,
  )

  if (highTemp < 0) return 'Brr!'
  else if (highTemp < 20) return 'Chilly!'
  else if (highTemp < 30) return 'Noice!'
  else return 'Hot!'
}
```

Despite triggering a network call in the production code, this function is easy to test. It's isolated from the `WeatherService` implementation, and doesn't need to know about how the service was constructed with a base URI and is making a network call. All it cares about is that it gets something that conforms to the service interface.

### Mocks

You guessed where this was heading: mocks. But instead of the questionable practice of _mocking_ (as a verb), here you can use the actual best practice of _mocks_ (as a noun). As [José Valim recommends](http://blog.plataformatec.com.br/2015/10/mocks-and-explicit-contracts/), we can swap out the real service implementation for a mock that conforms to the same interface. For example:

```javascript
type Responses = {|
  getForecast?: Forecast[][],
|}

export default function mockWeatherService(
  responses: Responses,
): WeatherService {
  return new MockWeatherService(responses)
}

class MockWeatherService {
  responses: Responses

  constructor(responses: Responses) {
    this.responses = responses
  }

  async getForecast(
    city: string,
    numDays: number,
    tempUnit?: TempUnit,
  ): Promise<Forecast[]> {
    return this.responses.getForecast ?
      this.responses.getForecast.shift() :
      unhandled({city, numDays, tempUnit})
  }
}

function unhandled<A>(args: Object): A {
  throw new Error(`Unhandled request with arguments: ${args}`)
}
```

Now, a unit test can look like this:

```javascript
describe('weatherMessage', () =>
  it('works for sub-zero', () =>
    expect(weatherMessage(mockWeatherService({
      getForecast: [[{
        city: '',
        day: {year: 0, month: 0, day: 0},
        tempUnit: 'C',
        highTemp: -1,
        /* This obviously doesn't make sense but it doesn't matter for
           this test */
        lowTemp: 0,
      }]]
    }))).toMatchSnapshot()
  )
)
```

The snapshot should be: `Brr!`.

A few different things are happening here:

- We set up a mock weather service that is constructed with mock responses (with the type `Responses`. Interestingly, this is an exact object type, not an interface, because we want a compile error if we pass in a wrong response name.)
- It responds with the responses, popping them out of the response array one by one, as `weatherMessage` triggers service calls
- If there are no responses for a service call, the mock service throws an 'unhandled request' exception, so that the test fails

This mock enables us to start writing tests quickly. We can start with a minimal, failing test:

```javascript
expect(weatherMessage(mockWeatherService({})))
  .toMatchSnapshot()
```

... and get an 'unhandled request' exception that tells us which response mock we need to provide next. If you have service mocks set up like this, writing unit tests for e.g. controllers in an Express app would involve a series of test exceptions that would _guide_ you through the process of setting up mock responses. The responses you provide would determine which branch of controller logic was taken and thus which service calls (if any) would be triggered next. The test writing process, as you can imagine, would be almost _interactive._

## Interfaces–the good parts

Of course, interfaces are not a magic bullet. You'll notice that even in the above examples I didn't use them for every type definition. Used judiciously, they pack quite a lot of punch in the ways I described. But they have their limitations.

I think we're fortunate in the JavaScript world that we have type systems that let us model JavaScript in a (relatively) safe way. One of the modelling tools they got absolutely right is interfaces. Remember that Flow/TS types are purely compile-time constructs, they are erased from the output JavaScript. With such low syntactic and runtime cost, interfaces and other types let us manage complexity and achieve quality while still shipping features for our users.