44

Adventures in static typing: React, Redux, Flow, oh my.

 5 years ago
source link: https://www.tuicool.com/articles/hit/mQNB73q
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Jvayuym.jpg!webaYZRJny.jpg!web
Illustration by Kayla Farrell

One of the largest single-page apps atWeWork consists of roughly 140K lines of code. A myriad of React components connected with Redux (and Reflux, in some of the older, to-be-refactored parts), juggling data from various backend services, styled with different systems (CSS modules, Radium), sprinkled with dozens of utility libraries.

Managing a beast of such size is no small feat.

Enter static typing — a language feature known to reduce the number of times someone has to fix a critical bug in the middle of the night, swearing to never write a single line of code again. In recent years, type annotations gained popularity in the front-end world, thanks to the excellent tools like Flow and TypeScript .

The story that follows is one filled with tears of both joy and sorrow, as we’ve spent the last nine months annotating the app with Flow .

The biggest takeaway? Adding end-to-end static typing to a React/Redux app is a lot of work — you’ll run into issues with Flow, third-party annotations, duplication, broken types along the boundaries of decorated or exported components, and a severe lack of documentation on how it should all be wired together. Nevertheless, the end result will be an undoubtedly higher-quality codebase that’s easier to work with and is more secure.

Below, we’ll talk about the bugs caught in our app, approaches on covering large codebases, the gotchas, and limitations of Flow, the issues with Redux, and all the things in between. This article is meant to make your Flow adoption easier.

How did this all even work before?

Getting started with Flow is easy — plug the directive on top of a file, annotate function arguments and you’re good to go. Having done that, we’ve started discovering various issues in the codebase. Here are some of the most notable ones:

Deep property access

There were quite a few cases where deeply nested properties were being accessed unsafely. It’s good practice, in general, to only “touch” local properties (also known as a Law of Demeter ). We weren’t being rigorous enough with this convention and Flow immediately exposed all of the offenders:

type Props = {|
  // …
  building?: { /* … */ } // building is optional!
|};
<input value={this.props.building.defaultLocale} …>

Naturally, this fixed lots of “Cannot read property … of undefined” errors in our tracking platform .

Below, we’ll touch more on property access and the issues with shorthands like Lodash’s get

Unused style properties

While we’re in the process of transitioning an app to CSS modules, there are still places that use plain JS objects for CSS styles (with the help of Radium). With Flow we were able to find missing declarations like this:

const style = {
  // …
  errorHeader: {
    color: colorRed,
    marginRight: ‘1em’,
  },
};
// …
render() {
  return (
    <div style={style.errorSubHeader}>
      Please correct these rows before uploading
    </div>
  );
}

Note invalid style here ( errorSubHeader , not errorHeader ).

Unused methods

While ESLint is great at catching unused or undeclared variables, it falls short in a more extensive analysis that static type checkers were meant to excel at. Flow found cases like this:

componentWillReceiveProps(nextProps) {
  if (this.props.officesCSV !== nextProps.officesCSV) {
    // method below no longer exists in a component
    // and the bug is likely hidden due to a condition 
    // that occurs infrequently
    this.validateOfficeCSV(nextProps.officesCSV);
  }
}

Incorrect property names

In the same vein, we found cases of incorrectly-named properties that either never worked or worked by accident:

const formattedCreatedAt = standardFormatDate(createdAt, {
  show_today: false,
  hide_current_year: true
});

Actual function signature looked like this:

function standardFormatDate(date, {
  showToday = true, 
  hideCurrentYear = false
}) {
  // …
}

The names must have been camel-cased during refactoring and the change somehow slipped through. Type checker to the rescue!

Similarly, we’ve discovered third-party libraries that were used incorrectly:

// incorrect usage
Rollbar.warning('Tour does not exist', JSON.stringify(this.props));
// correct usage (2nd argument is an error-like object)
Rollbar.warning('Tour does not exist', { 
  message: JSON.stringify(this.props) 
});

Inconsistent API

Speaking of method signatures…

type Props = {|
  onAssignTour: Tour => Promise<void>
  onCancelTour: (id: string) => Promise<void>
  // …
|};

Adding type annotations has an interesting side effect of exposing method signatures at a bird’s-eye view . In this example, we found that tour-related methods either accepted a tour object or a string representing its uuid . While this may not seem like a big deal, POLS ( Principle Of Least Surprise ) suggests that it’d be best to normalize method signatures when possible.

Inconsistent object shapes

In classic OOP, shapes are often determined by classes. Even though JavaScript now has classes, it also makes it trivial to create objects on the fly without more rigorous “instantiation”. It’s easy to get into a fragile state of passing arbitrary objects without any kind of structural integrity:

serializeMoveInReservation(obj) {
  const reservable = obj.reservables[0];
  return {
    action: 'add',
    buildingName: reservable.location_name,
    capacity: reservable.capacity,
    country: reservable.country,
    // …
  };
}

If we’re not using classes, this structural integrity can be ensured via Flow.

serializeMoveInReservation(obj: Reservation): SerializedReservation {
 // …
}

This gives confidence that the shapes never accidentally diverge across multiple parts of the system.

But there are so many files!

Hopefully, it’s clear by now that Flow is, well, quite useful. But how to approach wiring hundreds and hundreds of files?

Incremental annotations

The codebase doesn’t have to be annotated in one fell swoop. We’ve started by adding // @flow to some of the more critical files/features, slowly covering more and more. Another big help was starting with more generic annotations; in some cases, it would take too long to type objects and functions with proper (deep) signatures.

The strategy was:

  • Use mixed for values that aren’t easily determined (or any , in very rare cases), then replace with concrete types.
  • Use Function annotations, then replace with concrete types. E.g. (string) => Promise<void> or reusable, domain-level types ( FetchReservations ).
  • Use Object annotations, then replace with concrete types. E.g. { uuid: string } or reusable, domain-level types ( Reservation )
  • Define objects as loose ( { foo: ‘bar’ } ), then replace with more exact ones ( {| foo: ‘bar’ |} ) where applicable.
  • Add annotations for all the 3rd party libraries (moment, react-select, etc.)
  • Add $FlowFixMe in rare cases, then revisit individually
  • Try to add ”// @flow strict” where possible, starting from the “leaf nodes” (modules that don’t depend on anything)
  • Use proptypes-to-flow codemod to port components quicker

This allowed us to Flowify things in layers.

Linter

It’s important that every layer is also “sealed” with a corresponding lint rule. Getting rid of all the generic, non-type-safe Function is useless if there isn’t an automatic check preventing it from being accidentally added in the future by a team member unfamiliar with Flow.

eslint-plugin-flowtype does just that and is highly configurable. We’ve also contributed few rules to the project .

Mistakes, gotchas, lessons learned

Annotating objects

One thing we didn’t realize until later is that Flow is really good at figuring out the types of data structures . In the example below, adding type annotation actually makes things worse!

export const chartData: Array<Object> = [
  { date: ‘2017–01–01’, desksOccupied: 10, category: 3 },
  { date: ‘2017–01–02’, desksOccupied: 11, category: 1 }
  // …
];

Since Object is unsafe, omitting annotation altogether is better for preserving the type of this object. Even a specific type like Array<{}> or Array<{ [key: string]: mixed }> would be less safe. When there’s no type, Flow infers this structure as:

chartData: Array < {|
  date: string,
  desksOccupied: number,
  category: number
|} >

The ideal solution is to create a proper type:

type ChartData = Array<{
  date: string,
  desksOccupied: number,
  category: number
}>;
export const chartData: ChartData = [
 /* … */
];

Types are confusing

Additional confusion stems from the fact that Object and Function are essentially treated as any in Flow. This is not very intuitive for new developers. As we’ve transitioned away from Object’ s for generic “hashes,” we ended up creating a global helper:

// key defaults to string, the value is specified as generic
export type Hash<V, K = string> = { [K]: V };
type Props = {|
  style: Hash<string>,
  activeReservations: Hash<Array<Reservation>>
|};

Flow documentation sort of hints at { [key: string]: ... } notation but doesn’t give any guidance on what to do with mixed value types. Many libraries settle on { [string]: any } or { [string]: mixed } but both have issues — the former is not “strict” enough due to any and the latter often makes it impossible to use with more exact types.

Similarly, it’s unclear how to annotate generic functions. We’re told that Function is unsafe and that () => mixed should be used instead, but that breaks down the moment you start dealing with variadic functions. Spread syntax helps but, for some reason, needs to be defined as any (unlike the return value) or it also breaks down:

type Fn = (...args: Iterable<mixed>) => mixed;
const foo: Fn = (x: string) => {};
// ^ Cannot assign function to `foo`
// because string [1]
// is incompatible with mixed [2].

Finally, there’s a lot of confusion around intersection type vs. spreading (which Flow barely mentions in the docs ), especially as it relates to Props and decorated / higher-order components.

Should you use type Props = OwnProps & HOCProps or type Props = {| ...OwnProps, ...HOCProps |} . What are the differences? When should one be used over the other?

For example, we found that the order of spreading matters when it comes to the same-named properties:

type A = {| foo: number |};
type B = {| foo: string |};
// the following works
// reversing A and B results in foo being either number or string
type C = {| ...A, ...B |}; 
const test1: C = { foo: '' };
// this errors
// reversing doesn't matter and name conflict is exposed immediately
type D = A & B; 
const test2: D = { foo: '' };

This suggests that & is generally a safer way of combining shapes since it won’t silently “overwrite” them. Yet, for Props , spreading is generally the recommended way of combining them.

Inexact props are not safe

Flow has a concept of exact object types in which objects are only allowed to have an exact set of properties.

The difference between exact and inexact might not seem like a big deal at first, but it is actually incredibly important. Let’s take a look at an example of a React component that defines prop types as an inexact object:

type Props = {
  name: string,
  active?: boolean
};
const Person = ({ name }: Props) => <div>{name}</div>;
<Person name="foo" isActive />;

Inexact object allows us to instantiate a component with extra props, even those that are never used by the component. This can mask typos and introduce bugs after refactoring.

Making props exact fixes this problem:

type Props = {|
  name: string,
  active?: boolean
|};
const Person = ({ name }: Props) => <div>{name}</div>;
<Person name="foo" isActive />;
// ^ Cannot create `Person` element because property `isActive` is missing in `Props` [1] but exists in props [2].

Objects being inexact by default is arguably one of the biggest design mistakes in Flow. Charlie Koster talked about this in his “ Why I was looking forward to Flow, and then I wasn’t ”.

Good news is — Flow team is aware of this and is working on making them exact by default in the upcoming release . Meanwhile, we are working on making all objects exact to ensure prop safety. We’ve also added a lint rule that warns against inexact objects.

Note: there’s an issue in Flow at the moment where you can’t assign an empty object to an exact object with optional type:

type A = {| a?: string |};
// error: object literal. 
// Inexact type is incompatible with exact type
const x: A = {};

The workaround is to wrap it in $Shape<…> which marks every field as optional and preserves exactness.

type A = $Shape<{| a: string |}>;
const a: A = {}; // works
const b: A = { a: 'test' }; // works
// Cannot assign object literal to `c` because number [1] 
// is incompatible with string [2] in property `a`.
const c: A = { a: 1 };
// Cannot assign object literal to `d` because property `b` 
// is missing in object literal [1] but exists in `A` [2]
const d: A = { b: 'test' };

ReadOnly all the things

We all know how important immutability is when dealing with data. Flow allows its users to define properties as read-only using covariant notation :

type Reservation = {
  +uuid: string
};

Since we almost never mutate objects, most of the “shapes” are read-only. Moreover, the majority of annotations in a React app are those of Props and State. Props are immutable by default and state should never be mutated.

The correct annotation then looks like this:

type Props = {|
  +header: Hash<string>,
  +style?: Hash<string>,
  +emptyTableMessage?: string,
  +loading?: boolean,
  +refreshing?: boolean,
  +onRowClick?: () => void
|};

The + syntax is, arguably, rather unpleasant. We’ve recently learned that the same can be accomplished with a $ReadOnly utility:

type Props = $ReadOnly<{|
  header: Hash<string>,
  style?: Hash<string>,
  emptyTableMessage?: string,
  loading?: boolean,
  refreshing?: boolean,
  onRowClick?: () => void
|}>;

Much easier to read.

Duplication

One of the biggest annoyances you’ll run into when working with Flow is the duplication of property names when annotating data structures. One example is a Redux reducer:

const initialState = {
  loaded: false,
  data: [],
  error: null,
  currentUuid: null,
  appLocationModalName: ‘’
};
export const reducer = handleActions({
 [FETCH_EXPIRED_CONTRACTS_SUCCESS]: (state: State, action: Action) => ({
    ...state,
    loaded: true,
    error: null,
    byAccountUuid: {
      ...state.byAccountUuid,
      [action.meta.accountUuid]: action.payload
    }
  })
  // …
 },
 initialState
);

In order to create a State type, we initially ended up with tons of duplication:

type State = {|
  loaded: boolean,
  data: Array<{}>,
  error: ?boolean,
  currentUuid: ?string,
  appLocationModalName: ?string
|};

We later realized that we can utilize typeof operator :

type State = typeof initialState;

However, since Flow doesn’t know the types of initially null values, this requires some tweaking:

// spread the entire object AND annotate all null values
type State = {|
  ...typeof initialState,
  error: ?boolean,
  currentUuid: ?string
|};

This is obviously not ideal. It wouldn’t have happened if types were built into the language, but for now, this is the best solution we’ve come up with.

“enums” and ` Object.freeze `

There’s a common pattern of defining values as “enums,” essentially passing variables instead of strings. This often helps with safety and stability; a mistyped variable is caught by a linter, but a mistyped string isn’t.

export const ADD_OFFICES = 'addOffices';
export const DEAL = 'deal';
switch (selected) {
 case DEAL:
   this.props.openDealModal();
   break;
 case ADD_OFFICES:
   this.props.openAddOfficesModal();
   break;
 ...
}

Flow largely removes the need for this type of safety, since you can define union types and the compiler will check for their correctness.

An interesting gotcha was figuring out how to annotate these in a concise way. We found the following method helpful, as described in this Flow issue :

export const Options = Object.freeze({
 ADD_OFFICES: 'addOffices',
 DEAL: 'deal'
});
type OptionsType = $Values<typeof Options>;
const option: OptionsType = 'addOffices'; // works
const option2: OptionsType = 'foo'; // fails

Not only is this more concise, but you can still import Options and its values from the other modules (e.g. in tests).

Lodash, deep property access and optional chaining

Annotating all of the application files won’t give you complete safety. You have to pay attention to third-party libraries that are often stubbed out as any and can cause “holes” in the chain of function calls and values passing.

In some cases, this can be easily fixed by adding third-party type definitions. In other cases, things aren’t as trivial.

Earlier, I mentioned deep property access. In our app, we’ve often used Lodash’ get for this:

<input value={get(this.props, 'building.defaultLocale', '')} />

The problem is that you completely lose type safety when using get since Flow can’t infer what the return value is.

One way to solve this is by using an optional chaining operator (stage 1 proposal) and, since a few months ago, Flow now understands how to properly parse it.

<input value={this.props.?building.?defaultLocale || ''} />

Global objects

Since Flow doesn’t know about certain global objects, we had to stub out things like process.env , which we use to build the package with config variables:

declare class process {
 static env: { [string]: string };
}

and were then able to use it as such:

// config.js
export default {
 announcements: {
 uri: process.env.ANNOUNCEMENTS_URI,
 },
 // …
}
// avatar.jsx
import config from 'config';
// …
getAvatarUrl = () => {
  // …
  if (userInfo && userInfo.id) {
    return `${config.announcements.uri}/api/avatars/avatar_by_uuid?uuid=${userInfo.id}`;
  }
}

This way Flow could determine the exact type of config.announcements.uri in the interpolated string.

Same goes for window , which Flow doesn’t know about :

declare var window: {|
  location: Location,
  confirm: string => boolean,
  btoa: string => string,
  open: string => void,
  scrollTo: (number, number) => void,
  addEventListener: (string, Function, ?boolean) => void,
  removeEventListener: (string, Function, ?boolean) => void,
  requestAnimationFrame: Function => void
  // …
|};

Fun fact: TypeScript has window definitions but it might or might not crash your browser .

Type-safe CSS is not trivial

In our app, we mostly use CSS modules for styling.

// in index.scss
.wrapper {
 display: flex;
 flex-direction: row;
 align-items: center;
}
import style from './index.scss';
// …
render() {
  return (
    <div className={style.wrapper}>
    // …
    </div>
  );
}

A common way of Flowifying this is like so:

// in .flowconfig
[options]
module.name_mapper.extension='css' -> '<PROJECT_ROOT>/flow/CSSModule.js.flow'
module.name_mapper.extension='scss' -> '<PROJECT_ROOT>/flow/CSSModule.js.flow'

// in CSSModule.js.flow
// @flow strict
declare export default { [key: string]: string };

This gives us very minimal safety where Flow sees style as a generic “hash” { [key: string]: string } but it won’t catch mistyped, omitted, or non-existent classes.

More involved solutions exist and allow you to create exact objects, but it involves an extra build step:

// @flow
/* This file is automatically generated by css-modules-flow-types */
declare module.exports: {|
+myClass: string,
+primary: string
|};

Redux is popular, this should be easy…

In a React-Redux app, annotating components gives you only the basic level of safety. To achieve a true, end-to-end type checking, you need to make sure the entire Redux chain is tested through.

There doesn’t seem to be a definitive way of doing this, although few articles on the web offer solutions. In our experience, figuring out how to annotate Redux chain is an absolute nightmare (this assumes Flow <0.85 as the Redux types are currently undergoing few updates ).

At the very least, we need to:

  1. Annotate actions & action creators
  2. Annotate state in a reducer
  3. Annotate selectors
  4. Annotate connected components to work properly with both action creators and selectors

This picture ( courtesy of Tal Kol ) highlights the complexity: all of the edges in this graph must be checked by Flow.

vaeInmu.png!web3iuUF3A.png!web

Our stack also includes very common redux-thunk , reselect , and redux-actions libraries.

The full circle of annotation grows to an intimidating loop of hell:

  1. Actions
  2. Action creators
  3. redux-actions’ methods ( createActions , handleActions , combineActions )
  4. Thunks
  5. Reducers/State
  6. Regular selectors
  7. Compound selectors (via reselect)
  8. Connected components

Connected component definition

After a lot of exploration, we’ve settled on this kind of pattern:

type OwnProps = {|
 // …
|};
const mapStateToProps = (state: GlobalState, props: OwnProps) => ({
 // …
});
const mapDispatchToProps = (
 dispatch: Dispatch<BaseAction>,
 props: OwnProps
) => ({
 // …
});
type Props = {|
  ...OwnProps,
  ...ReduxProps<typeof mapStateToProps, typeof mapDispatchToProps>
  // any other types coming from decorators
|};
class Member extends Component<Props> {
 // …
}

This has a nice advantage of seeing a component’s own props at a glance. It also avoids duplication of “connected” props (defined in mapStateToProps and mapDispatchToProps ). The definition of those props is automatically inferred by Flow from selectors and action creators. No duplication!

// action creator is annotated with Flow
export const fetchContactInfo = (email: string) => (
 dispatch: Dispatch<BaseAction>,
 getState: GetGlobalState
) => {
 // …
 return dispatch(/* … */);
};
// in a connected component
import { fetchContactInfo } from '…';
const mapDispatchToProps = (
  dispatch: Dispatch<BaseAction>, 
  props: OwnProps
) => ({
  fetchContactInfo: () {
    // …
  }
});
type Props = {|
  ...ReduxProps<*, typeof mapDispatchToProps>
|};
class Member extends Component<Props> {
  // Flow knows that `fetchContactInfo` takes a string 
  // and returns either an action or a promise or a thunk 
  // (that returns an action or a promise or a thunk, recursively)
}

We define ReduxProps helper as follows (thanks to this discussion ):

export type ExtractReturn<Fn> = 
  $Call<<T>((...Iterable<any>) => T) => T, Fn>;
export type ReduxProps<M, D> = $ReadOnly<{|
  ...ExtractReturn<M>,
  ...ExtractReturn<D>
|}>;

One thing to note is that Redux’s signature variability makes annotating things with Flow a bit of a headache. For shorthand dispatch notation, we’re currently using this concoction:

export type ReduxPropsWithDispatchShorthand<M, D> = $ReadOnly<{|
  ...ExtractReturn<M>,
  ...$ObjMap<D, ExtractReturnWithDispatch>
|}>;

Reducer and actions

We had to make few tweaks to make redux-actions’ handleActions understand types properly. The signature of a reducer and action params is roughly as follows:

import { handleActions, type Reducer } from 'redux-actions';
// …
const initialState = {
 loaded: false,
 error: null,
 data: {}
};
type State = {|
 ...typeof initialState,
 error: ?string
|};
export const reducer: Reducer<State, *> = handleActions({
  [START_VERIFICATION]: (state: State) => ({
    ...state,
    loaded: false
  }),
[START_VERIFICATION_SUCCESS]: (
    state: State,
    action: ActionWithPayload<{ result: {} }>
 ) => ({
   ...state,
   loaded: true,
   data: action.payload.result,
   error: null
 }),
 
 [START_VERIFICATION_FAIL]: (
   state: State,
   action: ActionWithPayload<{ message: string }>
 ) => ({
   ...state,
   loaded: false,
   data: {},
   error: action.payload.message
 })
});

and the corresponding helpers:

export type BaseAction = 
  $ReadOnly<{ type: string, error?: string }>;
export type ActionWithPayload<P> = 
  $ReadOnly<{ ...BaseAction, payload: P }>;

Reducers and global state

Our global state is annotated in a very straightforward way. In the main file where all the reducers are combined together, we extract return values from each of its parts using that same ExtractReturn helper seen earlier:

import { combineReducers } from 'redux';
import accessControl from './accessControl';
import accounts from './accounts';
// more imports
const rootReducer = {
 accessControl,
 accounts
 // more reducers
};
export type GlobalState = 
  $ObjMap<typeof rootReducer, ExtractReturn>;
export default combineReducers(rootReducer);

GlobalState now represents the shape of the entire redux tree! Going back to the connected component, this creates proper linking in the mapStateToProps :

import { type GlobalState } from 'redux/modules';
// …
const mapStateToProps = (state: GlobalState) => ({
  incidentTypes: state.bans.incidentTypes || [],
  locations: state.locations.data
});

In this case, if state.bans.incidentTypes were to be mistyped, Flow would complain about non-existent property. We now have a full type-tested chain of:

Component → mapStateToPropsGlobalState → reducer for a specific state → actions for that reducer → mapDispatchToProps

Are the types being lost?

Here’s an interesting fact: just because flow command has no errors, doesn’t mean everything is fully type-checked. Wrapping a component in a decorator could make it lose types. Importing a typed shape from another file could make it lose types. A faulty libdef could make it lose types .

Thanks to the tools like flow-for-vscode , we were able to find strange issues like this where a combination of spreading, importing, and connect() was untyping the shape.

MfiQzeb.png!webB3uQBfQ.png!web

Alternatively, you can use flow type-at-pos <fileName> <colNumber> <lineNumber> to find out how exactly the Flow “sees” certain shapes.

It would be nice to have better tooling/detection of “lost types.”

External packages

Flow has a central repository of library definitions and generally makes it easy to set them up in your project, either pulling real types or stubbing out those that don’t exist.

While most of the popular libraries are there, there are also a lot of gaps. We’re missing at least a couple dozen definitions — react-hotkeys, react-tooltip, react-user-media, recharts, clipboard, reflux — just to name a few.

Even when libdefs do exist, their quality and completeness are often subpar. A random examination of react-select shows that the Props are defined as non-exact, making it easy to misspell a prop and have it go unnoticed (as described earlier). It’s also missing some parts of the API, and there are unsafe types like Object (we’ve submitted a patch to fix these).

Last but not least, the community is having a hard time keeping libdefs in sync with the Flow releases. A relatively recent Flow 0.85 broke tons of libdefs , including those for redux . On one hand, it’s great to see Flow constantly improve safety and expose previously silent issues; on the other hand, it’s unclear when, by whom, and if all the libdefs will be fixed. Developers are left in the dark, fiddling with their own workarounds .

Shipping external types

In a typical web app, you’re likely to have your own packages included via npm. In our case, it’s Plasma — a reusable component library . How should those packages be annotated for external use ?

// how would Flow know that h3 and text are part of props 
// and have correct types
import { Header } from '@wework-dev/plasma';
<Header h3 text="Add member" />;

This is where the fun begins. Flow has an experimental gen-flow-files command, but it hasn’t been updated in a long time and doesn’t really work .

There are various workarounds for generating lib types. Kent Dodds describes some of them .

One of the solutions is to create .flow files that are simply a copy of your source files. Curiously, this is not even documented anywhere and the official docs have javascript:alert(“TODO”) in that section. :joy:

As mentioned in other blog posts, this increases npm package size significantly, and doesn’t work. We bundle Plasma into a single file and Flow doesn’t know which modules are exported and how to annotate them.

For now, we’ve decided to automatically generate flow-typed defs in Plasma using a bit of jscodeshift and flow’s type-at-pos magic. This is generally what a lot of libraries do (react-redux, lodash, etc.). It’s concerning that Flow doesn’t have adequate tools for such common tasks.

Type Coverage

As you gradually add types to the codebase, it’s nice to track overall type coverage in the codebase. We found flow-coverage-report to be a good tool for this. The metric can be accounted for in your team’s OKRs.

Using CodeCov, we were also able to track Flow coverage on a PR basis, similar to how it’s done for unit tests. If a PR is not fully type-checked, you’ll easily notice it via coverage delta.

Unfortunately, Flow’s coverage does not seem to care about external libraries. Whether those are annotated fully or are simply stubbed as any will not change the coverage percentage.

Future of Flow, TypeScript, and final verdict

A question that lately circulates in the front-end community, which might also be at the tip of your tongue — “why Flow and not TypeScript?” There’s a growing number of developers disappointed in Flow and a rapidly-increasing TypeScript adoption.

Jamie Kyle highlights some of the concerns about Flow , including lack of PR merges, unaddressed issues, and poor activity. This post sheds more light on the shenanigans around annotating the modern React/Redux stack. Integrating Flow is very painful, although it can be done. And while some issues eventually get fixed, a number of them have no clear resolution at this time.

The reason we chose Flow is simply that the situation was quite different a year ago. I had the opportunity to speak with a member of the TypeScript team at the Edge Web Summit last year. Their outlook was that both Flow and TS are nearly feature-identical and our choice doesn’t matter much. We went with Flow due to a seemingly better integration with the React ecosystem (after all, both are Facebook creations).

Even though we now have a well-typed, end-to-end system that’s miles safer than it was, the future of Flow and our adoption of it is uncertain. Yes, the releases are done at a regular pace , the type safety keeps getting better and better, and the issues are addressed once in a while. Still, the lack of documentation and a clear roadmap, the out-of-sync ecosystem, and the non-existent or broken tooling are often painful enough to make one wonder if this is the right way forward. We really hope Flow will improve in the near future.

Hopefully, this article helps make your adoption of it easier.

Feel free to reach out to me on Twitter for any questions or clarifications.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK