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());
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": []
}
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)
};
};
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);
}
});
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));
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));
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)
));
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.