Using flow-runtime to enforce strict type constraints at runtime

Name one relatively unknown project that everyone should be using? flow-runtime

Take Flow to the runtime journey.

I have been using Flow in every project I have created in the last two years. Strict type checking, whether it is Flow or TypeScript, is awesome. I aim for 80%+ type coverage across the project. I will go as far as to say that using strict types makes majority of the usual test cases redundant, esp. those that check contract between different parts of the application. That is, unless your application is consuming third-party data.

Compilation-time type checking shortcomings

Strict type checking happens at the compilation time. Type annotations are either stripped or commented out at the runtime. This is Okay-ish if your application works with a known set of inputs, e.g. it is relatively safe to assume that the type annotations for a DOM keypress event are going to be representative of the event payload, at least in the modern browsers. However, compilation-time strict type checking gives no guarantees that type definition of a third-party data interface is going to respected, e.g.

type WeatherResponseType = {|

+temperature: number

|}; const weatherResponseHead = await fetch(' https://weather.api' ); const weatherResponse: WeatherResponseType = await weatherResponseHead.json();

There are absolutely no guarantees that weatherResponse is going to return this response in the future. Type annotations still provide a value here — your application knows the shape of the response and can validate the contract at the build time. However, compilation time checking is not going to help if the API produces an unexpected response in the future. This is where the flow-runtime comes in.

Runtime type checking

flow-runtime provides a Babel plugin to transpile Flow type annotations into runtime checks that use flow-runtime package to construct and evaluate assertions.

{

"plugins": [

[

"flow-runtime",

{

"annotate": true,

"assert": true

}

]

]

}

In the case of the earlier example, the transpiled code becomes:

import t from 'flow-runtime'; const WeatherResponseType = t.type('WeatherResponseType', t.exactObject(t.property('temperature', t.number())));

const weatherResponse = WeatherResponseType.assert((await weatherResponseHead.json())); const weatherResponseHead = await fetch(' https://weather.api' );const weatherResponse = WeatherResponseType.assert((await weatherResponseHead.json()));

As you can see, WeatherResponseType type annotation has been converted to a collection of functions used to assert the shape of the input at the runtime.

Continuing with out example, suppose that the API response changed to:

{

"temperatureCelsius": 30,

"temperatureFahrenheit": 86

}

In this case, Flow will throw an error in the following format:

WeatherResponseType should not contain the key: "temperatureCelsius" Expected: {|

temperature: number;

|} Actual: {

temperatureCelsius: number;

temperatureFahrenheit: number;

} ------------------------------------------------- WeatherResponseType should not contain the key: "temperatureFahrenheit" Expected: {|

temperature: number;

|} Actual: {

temperatureCelsius: number;

temperatureFahrenheit: number;

}

Importance of catching the error early

You might be thinking – if API produced an unexpected response, the program will fail anyway. How is flow-runtime assertion going to help apart from producing a useful error message?

Your program will fail only if you are lucky. With an exception of limited scenarios where JavaScript is going to throw an error (e.g. attempt to access a property of undefined ), JavaScript is going to quietly perform value coercion This has bitten me quite a few times when I was expecting an external API to return an identifier, e.g.

{

"id": 1,

"name": "Foo"

}

My application would expect the above output and use it to construct URL of a resource, e.g.

type VenueType = {|

+id: number,

+name: string

|};

const url = ' const venue: VenueType = await getVenueById(1);const url = ' https://go2cinema.com/venues/' + venue.id + '-' + slugify(venue.name);

Then suddenly, the API changes its response format to:

{

"name": "Foo",

"venueId": 1

}

Without flow-runtime , the program is going to continue to quietly construct URLs as https://go2cinema.com/venues/undefined-vue-shepherds-bush (notice the undefined ).

The cost of runtime assertions

Runtime type assertions are inlined for the parameters, variable assignment with type association (such as in the provided example) and function return result. This adds many additional CPU cycles to every computation. Therefore, I would avoid using flow-runtime in production, in consumer facing services. I am using flow-runtime for all of the background tasks. You can use runtime checking during development and disable it in production.

Weighting the benefits of runtime assertions

The last 9 months I have been developing https://go2cinema.com/ – a cinema ticket booking platform that now covers 70% of the UK cinemas and aggregates data for 99% of the UK cinemas and majority of the cinemas in Germany, France and Spain. If it sounds like we are dealing with a lot of data feeds, then its because we do (over 300 to date). My first attempt was to define a contract with each data feed using Flow type annotations and JSON schema. This works. However, in almost all of the cases, JSON schema duplicates the Flow type declarations. By switching to flow-runtime I got rid of most of the JSON schemas.

Overall, I’d estimate that replacing JSON schemas with flow-runtime reduced the codebase size of the feed aggregators by +30%. Meanwhile, flow-runtime is continually proving itself to be the most-valuable-player at discovering bugs.

Author

flow-runtime is authored by Charles Pick (AKA phpnode). I have been using flow-runtime in every project that heavily depends on consuming third-party data feeds and for that I think Charles deserves as a very least an hour of my time to write a shout-out to his awesome project.

Thank you Charles!