Flow type

We are convinced that statically typing

Our needs

When we began developping our application, there were not much available tools to provide static typing for Javascript. To be brief, there were two decent available tools : Flow and Typescript. If we had chosen to use Typescript, we would have had to use it to write our whole codebase, from the very start, and use its related tooling (such as TSC to compile). We chose not to do it, because of the lack of tools for React that existed around the Typescript ecosystem at that time. For example, we use ESLint to analyse code style. There is, of course, TSLint, but the TSLint rules for React only exist since July 2016, which was almost one year after our project started. Flow, on the other hand, allowed us to write vanilla JS, and progressively add static typing to our codebase, when we felt a module required it. This allowed us to reduce the overhead and the tooling weight when we started, but also allowed us faster refactoring and better bug detection by integrating Flow in our CI process.

We now use Flow on a daily basis, trying to add type definitions to as much modules as possible. However, this is more of a code improvement practice than a strict obligation. For those of us who use the TDD cycle (write test - write code - refactor), Flow types would often take place in the "refactor" part.

Configuration

Flow needs to be configured by placing an ini-like file named .flowconfig at the root of your project. Then, when running the binary, Flow will look for all Javascript files in the project folders, including the ones in node_modules. This is a behaviour that you probably want to keep. This way, when a module includes flow annotations, you can leverage them immediately. Even when a module doesn't export type annotations, flow is sometimes able to infer enough to help you make sure you are using the module correctly. That being said, some node_modules are known to cause unexpected problems with Flow. Below is an extract of our .flowconfig, with a list of modules we had to ignore, and the specific options we had to enable to make Flow work with our coding style.

[ignore]
.*/node_modules/debug/.*
.*/node_modules/node-uuid/.*
.*/node_modules/querystring/.*
.*/node_modules/react-fontawesome/.*
.*/node_modules/redux-router/.*
.*/node_modules/babylon/.*
.*/node_modules/fbjs/.*
.*/node_modules/radium/.*
.*/node_modules/react-color/.*
.*/node_modules/react-motion/.*
[options]
suppress_comment= \\(.\\|\n\\)*\\$FlowFixMe
esproposal.class_static_fields=enable
esproposal.class_instance_fields=enable

External type definitions

Some modules don't export type definitions, so we included custom type definitions to work more easily with them. Some of them are relative to modules that we use and maintain internally, and some are related to NPM modules, and are heavily inspired by flow-typed (however, we did not use flow-typed because it was too "magic" from our point of view).

# .flowconfig
[libs]
interfaces/

Here is an extract from the structure of our interfaces folder :

interfaces
  ├── ...
  ├── mocha_v2.4.x.js
  ├── querystring.js
  ├── react-fontawesome.js
  ├── redux-router.js
  ├── test-globals.js // globals available in the tests context that we wanted to type using Flow, without having to import them
  ├── url-join.js
  └── ...

Each one of these files exports a type declaration using Flow's declare syntax.

How to publish module that is flow enabled

Deciding where to place a flow type declaration

When we had to declare types that were relative to our business logic, we wanted the declarations to be situated at the root of our source code directory structure, so that we could import it from a lower place of our directory structure and add typing at the places where said data would be used. Hence, we put them in a types directory, located at the root of our src. To learn more about our policy on directory structure and modules location, read the article about this specific topic.

On the other hand, when needing to create a new type derived from a business-logic type (e.g for refined data), we often created it in our component's folder structure, creating a types.js file when there were many declarations. This guaranteed that those type declaration would never be confused with the "pure" ones, tightly bound to business logic and to our backend's responses scheme.

Adding Flow type declarations to the Redux state store

A point that caused us a little trouble was about adding flow to our state store. What we did was typing the local parts of state managed by each reducer, inside the reducer's file :

// fooReducer.js
export type State = {
  bar: string,
  baz: number,
}

To abstract the position of a specific reducer in the global state store, thanks to a getLocalState method, defined alongside our reducer (as a side note, this also helped us refactor our state store more easily, by having a single line to change in our code when we wanted to move a reducer around) :

// fooReducer.js
export const getLocalState = (globalState: Object): State => {
  return globalState.someSpecificPart.foo;
}

This enabled us to flow type our local state everywhere we used it, especially when connecting our components with react-redux connect function, and making flow stop us from trying to access undefined parts of the local state :

// FooComponent.js

import { getLocalState } from './fooReducer.js';

type Props = {
  bar: string,
  baz: number,
  someProp: bool,
};

const DumbFooComponent = (props: Props) => { /* ... */ };

const mapStateToProps = (state: Object): Props => {
  // state is the global application state
  const localState = getLocalState(state);
  return {
    bar: localState.bar,
    baz: localState.baz,
    someProp: localState.nonExistingPartOfState, // Flow triggers an error on this line thanks to getLocalState
  };
}

export default connect(mapStateToProp)(DumbFooComponent);

Note that, when a component needs to be aware of parts of application state that its "own" reducer (e.g the reducer for the part of the app it belong to), we may easily import other getLocalState functions (using import as) and benefit form flow types for parts of the state defined anywhere in the app, enabling us easier refactorings.

Conclusion

Flow suited us well, because it allowed us to progressively iterate and to add type definitions to code that wasn't written with types in the first place. We progressively added flow types to our ever-growing codebase. It helped us move faster by making us more confident in our refactorings, warning us when importing non-existing files or accessing undefined parts of objects. This article shows the ways we found to make the best use of it. The next one is to be seen as a subsection of this one, and presents specific recipes and patterns you may find useful when working with Flow.

results matching ""

    No results matching ""