Shipping Flowtype Definitions in NPM Packages

It’s super awesome to see a lot of libraries starting to adopt flow to add type-safetiness to their code…

BUT… what a lot of people forget is that npm packages usually ship ES5 code without any type information and in the end, the consumer of this library will only get any as imported types.

While working on the typings in styled-components, I have been looking for a guide on how to expose flow types for the user the right way and realized that there wasn’t any yet.

So this article aims to give library maintainers some guidelines / ideas on how to vendor flow definitions within the npm build process.

As a reference project, I created a repository with some bare bone package setup and some example code.

We will go through the setup step-by-step and also talk a little bit about some flow specific mechanics, e.g. how flow imports files.

So let’s get started!

Our example library my-lib consists of a src directory with some ES6 source code:

$ tree src
src
├── index.js
└── util
├── __tests__
│ └── reduceChainPromises-test.js
└── reduceChainPromises.js <-- showcasing a nested subdir

As maintainers we usually want to distribute our code as…

  1. … web compatible format (UMD, AMD,…) in dist
  2. … ES5 compiled source in lib (keeping the file structure of src)

We will completely ignore case 1, since as for right now, there is no easy way to expose type information for a single compiled file (except for writing a libdef file). In this article, we will focus on 2, how to make flow recognize types for files shipped in lib.

In our first step, we will install all necessary devDependencies to get our build chain going:

npm install babel-cli babel-core babel-preset-es2015 babel-plugin-transform-flow-strip-types flow-copy-source rimraf --save-dev

If you have already been using babel, then you are probably familiar with most of these dependencies. The babel plugin babel-plugin-transform-flow-strip-types will make sure that all flow related code will be stripped out in our compiled ES5 code.

Additionally, we install rimraf for running rm -rf commands and flow-copy-source to easily create so-called “flow-files”, which will be explained later on.

The most interesting part is, that all our tools are running on node so we don’t have to worry about any Windows / OSX / Linux development setups.

For completeness, here is our .babelrc:

{
"presets": ["es2015"],
"plugins": ["transform-flow-strip-types"]
}

Alright, now babel understands ES6 + flow syntax! Let’s go through our package.json scripts next:

{
"name": "my-lib",
...
"scripts": {
"test": "jest",
"build": "npm run build:clean && npm run build:lib && npm run
build:flow",
"build:clean": "rimraf lib",
"build:lib": "babel -d lib src --ignore '**/__tests__/**'",
"build:flow": "flow-copy-source -v -i '**/__tests__/**' src lib"
}
...
}

In our scripts section, we can find thebuild instruction, which will do three things:

  1. npm run build:clean: Removes the current lib directory
  2. npm run build:lib: Builds lib from scratch by compiling src via babel
  3. npm run build:flow : Creates our flow-files, which are also put into lib

Step 3) is relevant for our goal… we call flow-copy-source which will (by default) copy all *.js files in src and copy them into the target directory lib , preserving the original directory hierarchy. Those files will have a different file-ending, called *.js.flow.

Side Note:
I am using jest for testing, that’s why I added ignore rules for **/__tests__/** globs. I don’t want test files to land in lib.

Here is a listing of our resulting lib after running npm run build:

$ tree lib
lib
├── index.js
├── index.js.flow <-- Exact same content as src/index.js
└── util
├── reduceChainPromises.js
└── reduceChainPromises.js.flow

Introducing “flow-files” (*.js.flow)

So if there is a file src/util/reduceChainPromises.js, the whole file will be copied to lib/util/reduceChainPromises.js.flow … but why do we need that?

Flowtype resolves modules the same way as node does. If you are importing my-lib/lib/util/reduceChainPromises, you would probably do it like this:

import reduceChainP from 'my-lib/lib/util/reduceChainPromises';

By default, flow will look into node_modules/my-lib and try to find the file lib/util/reduceChainPromises. But wait! Files in lib only contain pure ES5 code, so we loose our type information (flow will most likely treat reduceChainP as type any).

Luckily, if flow finds files with a *.js.flow file ending, it will prefer it over the actual *.js. So we vendor additional files, which will only tell flow what types are exposed in the accompanied ES5 file. Since these files are generated in our build process, we can always be sure that our flow-files are synced up with the actual code.

What about the file bloat?

You will probably argue that copying the whole src files to lib will unnecessarily bloat the resulting npm package, right? The flow team is aware of that issue and is working on an experimental command called flow gen-flow-files:

$ flow gen-flow-files --help
Usage (EXPERIMENTAL): flow gen-flow-files [OPTIONS] [FILE]
e.g. flow gen-flow-files ./src/foo.js > ./dist/foo.js.flow

As soon as this feature is stable, we will be able to extract pure type information from our ES6 code, and put those type information in our *.js.flow, ultimately stripping the implementation code (kinda like the invert of what babel does to our src files).


Okay nice, so we now have a build system which will generate backwards compatible ES5 code AND add type definitions for our library, ready to be used by the consumers.

This was just a very basic introduction to this topic.

There are still a lot of open questions:

  • What about dependencies for our library?
  • What about dependency collisions with our library (e.g. lodash)?
  • What about different flow versions between the consumer & my library?

As soon as I find some time, I will do some follow up articles where we will deep dive into these problems and discover how to solve them as well!

You can check out our flow-support docs on styled-components to see how a more complex library is being typed and shipped with flow.


For questions or more updates on Flowtype leave me a tweet or follow me on Twitter @ryyppy