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…
- … web compatible format (UMD, AMD,…) in
dist
- … ES5 compiled source in
lib
(keeping the file structure ofsrc
)
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:
npm run build:clean
: Removes the currentlib
directorynpm run build:lib
: Buildslib
from scratch by compilingsrc
via babelnpm run build:flow
: Creates our flow-files, which are also put intolib
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 usingjest
for testing, that’s why I added ignore rules for**/__tests__/**
globs. I don’t want test files to land inlib
.
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