Safely use untyped values in Flow

Safely use untyped values in Flow

By Guillaume Claret

At OuiCar, we use the static type checker FlowType to detect bugs in our Javascript applications. Since we have a lot of historical code or third party libraries, our code is a mix of typed Javascript and untyped Javascript. This can be the source of some subtle bugs, like runtime type errors in the typed code. We present a small assertion library in order to help to better embed untyped values in Flow programs.

Decode

We designed a Decode library, whose code is given in the appendix below. The aim of the Decode library is to dynamically check that some untyped value has some expected type. This is especially useful to check that our server API returns well-formed results.

Here is the Gist of it:

import * as Decode from 'path/to/decode';

// The type of our API results.
type ApiAnswer = {
	brand: string,
	id: number,
	passengers: string[]
};

// An example of API result. Since this value is intended to be a JSON given
// by the server at runtime, its type is `mixed`.
const apiAnswer: mixed = {
	brand: 'Renault',
	id: 428,
	passengers: ['Paul', 'Edouard']
};

// The `valitatedAnswer` have the same value as `apiAnswer`, but with
// the right Flow type. Its type is checked at runtime (would raises an
// exception in case of mismatch).
const valitatedAnswer: ApiAnswer = {
	brand: Decode.field(apiAnswer, 'brand', Decode.string),
	id: Decode.field(apiAnswer, 'id', Decode.number),
	passengers: Decode.field(apiAnswer, 'passengers', Decode.array(Decode.string))
};

console.log(valitatedAnswer);
// ===> {brand: 'Renault', id: 428, passengers: ['Paul', 'Edouard']}

With an ill-formed API result:

// This API result is invalid (the `id` is a `string` instead of a `number`).
const apiAnswer: mixed = {
	brand: 'Renault',
	id: '428',
	passengers: ['Paul', 'Edouard']
};

const valitatedAnswer: ApiAnswer = {
	brand: Decode.field(apiAnswer, 'brand', Decode.string),
	id: Decode.field(apiAnswer, 'id', Decode.number),
	passengers: Decode.field(apiAnswer, 'passengers', Decode.array(Decode.string))
};
// ===> Exception: in key `id`: Decode: "428": not a number

Inspirations

The Decode library is inspired by the examples about the mixed type in the Flow documentation, and by the Decode library of Elm.

Appendix

This is the code of our Decode library. We did not have the time, but are planning to publish it on npm later.

// @flow
import _mapKeys from 'lodash/object/mapKeys';
import _mapValues from 'lodash/object/mapValues';

export type t<A> = (value: mixed) => A;

export function fail<A>(value: mixed, message: string): A {
	const valueMaxLength = 300;
	const valueMessage = JSON.stringify(value).substr(0, valueMaxLength);
	const valueMessageWithDots = valueMessage.length === valueMaxLength ?
		valueMessage + '(...)' :
		valueMessage;
	throw new Error(`Decode: ${valueMessageWithDots}: ${message}`);
}

export const any: t<any> = value =>
	value;

export const mixed: t<mixed> = value =>
	value;

export const bool: t<bool> = value =>
	typeof value === 'boolean' ?
		value :
		fail(value, 'not a boolean');

export const fun: t<Function> = value =>
	typeof value === 'function' ?
		value :
		fail(value, 'not a function');

export const number: t<number> = value =>
	typeof value === 'number' ?
		value :
		fail(value, 'not a number');

export const string: t<string> = value =>
	typeof value === 'string' ?
		value :
		fail(value, 'not a string');

export const void_: t<void> = value =>
	value === undefined ?
		value :
		fail(value, 'not undefined');

export function option<A>(decode: t<A>): t<?A> {
	return value =>
		value === null || value === undefined ?
			value :
			decode(value);
}

export function array<A>(decode: t<A>): t<A[]> {
	return value =>
		Array.isArray(value) ?
			value.map(element => decode(element)) :
			fail(value, 'not an array');
}

export function map<K, V>(decodeKey: t<K>, decodeValue: t<V>): t<{[key: K]: V}> {
	return value =>
		typeof value === 'object' && value !== null ?
			_mapValues(_mapKeys(value, (v, k) => decodeKey(k)), decodeValue) :
			fail(value, 'not an object');
}

export function field<A>(value: mixed, id: string, decode: t<A>): A {
	if (typeof value === 'object' && value !== null) {
		if (id in value) {
			try {
				return decode(value[id]);
			} catch (error) {
				return fail(value, `in key \`${id}\`: ${error.message}`);
			}
		}
		return fail(value, `missing field '${id}'`);
	}
	return fail(value, 'not an object');
}

export function optionalField<A>(value: mixed, id: string, decode: t<A>): void | A {
	if (typeof value === 'object' && value !== null) {
		if (value[id] !== undefined) {
			try {
				return decode(value[id]);
			} catch (error) {
				return fail(value, `in key \`${id}\`: ${error.message}`);
			}
		}
		return undefined;
	}
	return fail(value, 'not an object');
}

export function union<A, B>(decodeA: t<A>, decodeB: t<B>): t<A | B> {
	return value => {
		try {
			return decodeA(value);
		} catch (error) {
			return decodeB(value);
		}
	};
}
OuiCar's Picture

About OuiCar

OuiCar is a community market place to find and rent cars anywhere in France. But we are not just car renters. We also like to experiment new technologies and share ideas with others.

Paris http://www.ouicar.fr

Comments