Typesafe handling of remote APIs with Flow

08 Jul 2018

You built yourself a small typesafe program... as everything type-checks and Flow is patting you on the back, you are growing the program.

One day, you decide you need to talk to a remote server — a lot of programs these days do that.

But it seems like your perfect little castle is going to be filled with assassins and what not... I mean, you can't rely on the server upholding its end of the contract.

How do you keep the type safety that you fought so hard for, while when working with an API?

Let's take a super-simple example: we want to get the current user from the API. The response object looks something like this: { "id": 1, "name": "Monaca", "avatarUrl": null }.

Step 1. Make a type.

That object is a User, and we should give it a type it deserves:

type User = {
  id: number,
  name: string,
  avatarUrl: ?string,

Just enumerate all of its properties and their value types.

As a reminder, here's a list of primitive types Flow supports.

Step 2. What will this remote function be like?

Let's focus on the shape instead of implementation for now.

The result of an asynchronous action is often signaled using a Promise that will (eventually) contain the response from the server. The shape of that function (aka, its type signature) could look as follows:

() => Promise<User>

If you're thinking, "Wait a minute, what's this <> business?" here's a short summary:

A Promise is a generic type, much like Array. What this means is, you don't just have an Array of anything or a Promise of anything — you have an Array of numbers (Array<number>), or, in this case, a Promise of a User (Promise<User>).

We can define the function like this:

function getCurrentUser(): Promise<User> {
  return fetch('/me').then(x => x.json());

The return value of fetch is Promise<any>, and getCurrentUser casts it to Promise<User>... which is in the Danger Zone™. At this point, if you trust your API enough to always return a well-formed response, you might as well stop there. However, for most cases, you should not rely on the server. So read on.

Step 3. any -> User

We are not settling with any -> User conversion by merely casting... after all, this can lead to terrible bugs... because we are not sure yet if what we got is User indeed.

What can we even do? We can introduce another function, parseUser(user: any): User, which accept server response (as any). It will either return a User object if all is well, or it will throw an error if server response is of wrong shape.

We will use it like this:

function getCurrentUser(): Promise<User> {
  return fetch('/me').then(x => x.json()).then(parseUser);

The simplest implementation of the parseUser function would be:

function parseUser(user: any): User {
  if (
    typeof user.id === 'number' &&
    typeof user.name === 'string' &&
    (user.avatarUrl === null || typeof user.avatarUrl === 'string'
  ) {
    return user;
  } else {
    throw new Error('Could not parse User');

Yes. We are checking in runtime. And no, there's nothing wrong with it — because we are dealing with a value the type of which is only known at the runtime. There is no way around runtime checks.

And this could be just enough. You can go and do that in your apps.

The code for the parseUser function might feel somewhat manual. And it will only grow harder to read as we add more fields, or introduce parsers for other models.

That's something we can fix though.

Step 4. Composability.





This section is not ready yet.

Leave your email in the subscription box below 👇 to receive it once it's finished.

Think your friends would dig this article, too?

If you need a mobile app built for your business or your idea, there's a chance I could help you with that.
Leave your email here and I will get back to you shortly.