Shipping Flowtype Definitions in NPM Packages
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
│ └── 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
- … ES5 compiled source in
lib(keeping the file structure of
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
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
babel understands ES6 +
flow syntax! Let’s go through our
package.json scripts next:
"build": "npm run build:clean && npm run build:lib && npm run
"build:clean": "rimraf lib",
"build:lib": "babel -d lib src --ignore '**/__tests__/**'",
"build:flow": "flow-copy-source -v -i '**/__tests__/**' src lib"
scripts section, we can find the
build instruction, which will do three things:
npm run build:clean: Removes the current
npm run build:lib: Builds
libfrom scratch by compiling
npm run build:flow: Creates our flow-files, which are also put into
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
I am using
jestfor testing, that’s why I added ignore rules for
**/__tests__/**globs. I don’t want test files to land in
Here is a listing of our resulting
lib after running
npm run build:
$ tree lib
├── index.js.flow <-- Exact same content as src/index.js
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';
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
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 --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
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.
- What about different
flowversions 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
For questions or more updates on Flowtype leave me a tweet or follow me on Twitter @ryyppy