DEV Community

stereobooster
stereobooster

Posted on

Pragmatic types: IO validation or how to handle JSON-based APIs in statically typed language

Languages with static types need a special procedure to convert data from the outside (untyped) world (aka Input-Output or IO) to internal (typed) world. Otherwise, they will lose promised type safety. This procedure is called IO validation. Side note: the fact that system makes type checking at run-time means it is a dynamically-typed system, but this will be explained in another post.

A typical example of IO validation is parsing of JSON response from API.

Flow and TypeScript

Note: code looks identical in TypeScript and Flow

// @flow
type Person = {
  name: string;
};
// $FlowFixMe or @ts-ignore
const getPerson = (id: number): Promise<Person> =>
  fetch(`/persons/${id}`).then(x => x.json());
Enter fullscreen mode Exit fullscreen mode

We want that getPerson would return Promise of Person, and we tricked type system to believe that it always be the case, but in reality, it can be anything. What if API response look like:

{
  "data": { "name": "Jane" },
  "meta": []
}
Enter fullscreen mode Exit fullscreen mode

This would end up being runtime error somewhere in function which expects Person type. So even our static type system doesn't find errors they still potentially exist. Let's fix this by adding IO validation.

// it is guaranteed that this function will return a string
const isString = (x: any): string => {
  if (typeof x !== "string") throw new TypeError("not a string");
  return x;
};

// it is guaranteed that this function will return an object
const isObject = (x: any): { [key: string]: any } => {
  if (typeof x !== "object" || x === null) throw new TypeError("not an object");
  return x;
};

// it is guaranteed that this function will return an Person-type
const isPerson = (x: any): Person => {
  return {
    name: isString(isObject(x).name)
  };
};
Enter fullscreen mode Exit fullscreen mode

Now we have a function which will guaranteed return Person or throw an error, so we can do:

// without need to use $FlowFixMe
const getPerson = (id: number): Promise<Person> =>
  fetch(`/persons/${id}`)
    .then(x => x.json())
    .then(x => {
      try {
        return isPerson(x);
      } catch (e) {
        return Promise.reject(e);
      }
    });
Enter fullscreen mode Exit fullscreen mode

or if we take into account that any exception thrown inside Promise will turn into rejected promise we can write:

// without need to use $FlowFixMe
const getPerson = (id: number): Promise<Person> =>
  fetch(`/persons/${id}`)
    .then(x => x.json())
    .then(x => isPerson(x));
Enter fullscreen mode Exit fullscreen mode

This is the basic idea behind building a bridge between dynamic and static type systems. A full example in Flow is here. A full example in TypeScript is here

Libraries

It is not very convenient to write those kinds of validations every time by hand, instead, we can use some library to do it for us.

sarcastic for Flow

Minimal, possible to read the source and understand. Cons: misses the union type.

import is, { type AssertionType } from "sarcastic"
const PersonInterface = is.shape({
  name: is.string
});
type Person = AssertionType<typeof PersonInterface>
const assertPerson = (val: mixed): Person =>
  is(val, PersonInterface, "Person")
const getPerson = (id: number): Promise<Person> =>
  fetch(`/persons/${id}`)
    .then(x => x.json())
    .then(x => assertPerson(x));
Enter fullscreen mode Exit fullscreen mode

io-ts for TypeScript

Good, advanced, with FP in the heart.

import * as t from "io-ts"
const PersonInterface = t.type({
  name: t.string
});
type Person = t.TypeOf<typeof Person>
const getPerson = (id: number): Promise<Person> =>
  fetch(`/persons/${id}`)
    .then(x => x.json())
    .then(x => PersonInterface.decode(x).fold(
       l => Promise.reject(l),
       r => Promise.resolve(r)
     ));
Enter fullscreen mode Exit fullscreen mode

Generator

No need to write "IO validators" by hand, instead we can use tool to generate it from JSON response. Also, check type-o-rama for all kind of conversion of types. Generators with IO validation marked by box emoji.

This post is part of the series. Follow me on twitter and github.

Top comments (1)

Collapse
 
c01nd01r profile image
Stanislav

Thank you for the article!

It seems that the last example should have an

type Person = t.TypeOf<typeof PersonInterface>

instead of

type Person = t.TypeOf<typeof Person>