Flowtype導入のための指針・実際の運用について

  • 107
    いいね
  • 0
    コメント

このドキュメントの目的

自分は趣味でFlowをずっと使っていて、またプロダクションでも今まで3プロジェクトほどにFlowを導入した。その知見。

「Flow は便利そうだけど、怖い」「いれてみたら色々ハマったからクソ」「わからん、なにもかも…」という人に対し、自分がいままで出くわしたパターンや、聞かれた疑問について、メジャーな解法を提示する。

なぜFlowを導入するか

  • Babel から段階的に導入することが出来る
  • React の JSX にも推論を入れることができる
  • 部分的に適用できる
  • ASTがES準拠であり、ESLintなどがツールが使える(TSは独自AST)
  • それ自身ランタイムに全く影響はないので落とすのも簡単

実際にはReactと一緒に使うのが、エコシステムもユースケースも揃っていて、一番効果を発揮するだろう。それか、小さい npm モジュールを自分で書くとき。

型のメリット/デメリットについては、いろいろ各自思うところがあるでしょう。略。

TypeScript と違って、後付できる、ミニマムスタートできる、部分的にデタッチできる、nullable周りの推論器が優秀、あたりがメリット。このドキュメントの主は導入と運用の話で、TSとの比較も今回は省略。

導入

brew install flow

または

npm install flow-bin --save-dev
# or
yarn add flow-bin -D # 以下面倒なのでyarnのみで説明する

flow-bin はプラットフォームに応じたFlowのバイナリをインストールしてくれる。
グローバルに入れておくのもいいが、内部モジュールとしてバージョンを固定するほうが、集団開発でバージョンによるブレが発生しなくてよい。

$ flow init

で.flowconfigを生成する。

Babel の設定

yarn add babel-preset-flow  -D

他に、babel-preset-es2015 と一緒に使うなら、次のような設定になる。

.babelrc
{
  "presets": ["es2015", "flow"]
}

これは構文の解釈、コンパイル時の型アノテーションの除去を行うだけで、静的解析はFlow側がもっている。

babel-plugin-transform-class-properties

babel-preset-flowでも最小限動くが、これもほぼ必須。

yarn add babel-plugin-transform-class-properties  -D

transform-class-properties は必須ではないように見えるが、次のような頻出する構文で要求される

class X {
  a: number
}

ここの a: number の部分のパースは、babel上では、transform-class-properties の責務となる。

.babelrc
{
  "presets": ["es2015", "flow"],
  "plugins": ["transform-class-properties"]
}

Flow の実行

環境を作ったら、試してみよう。

foo.js
/* @flow */
var x: number = "text"

コマンドラインから flow または npm run flow を叩いて、エラーが出たら成功。node_modules 以下のエラーが出たら、次の 「.flowconfig の設定」を読むように。

初期設定では、 /* @flow */ のマジックコメントがあるもののみ Flow の管理下に入る。そうではないファイルを import した際は、参照は any 型で解決される。Flowの管理下に映すと、段階的に適応できる、というわけ。

Flow における構文の解釈

事前に知っておくべきこととして、Flow は OCaml で書かれた独自のパーサをもっていて、babel側がそれに合わせてやる必要があり、たとえば :: の bind 構文などはFlowが対応するまで、どう頑張っても認識できない。これはFlowを採用する範囲の構文に縛られることを意味する。

基本的には es201X の stage2 か stage3 のうち、Flow 側が取捨選択的に導入している。攻めた構文は採用できないか、そこだけFlow管理下から外したりする。

.flowconfig の設定

生成した設定は次のようになっていると思う。

.flowconfig
[ignore]

[include]

[libs]

[options]

この時点で、flow と叩いて、落ちることがある。それは node_modules下を推論しにいって、そのライブラリがFlowで書かれていて、型情報が足りなくて、落ちる。というケース。一番よく遭遇するのが stylelint。次に config-chain の「壊れたjsonに対するテスト」がFlowとして不正ということになって、死んでしまう。

こういうときは ignore する。

[ignore]
.*/node_modules/stylelint/.*
.*/node_modules/config-chain/test/.*

これで通るはず。

Flow の設定変更と再起動

注意点として、Flowは型情報の収集にバックグラウンドでサーバを走らせるのだが、.flowconfig をファイルを書き換えた際の検知がそこそこの確率で失敗している。困ったら flow stop; flow と叩いていることが多い。 flow restart もうまく動いていないことが多い。経験上。

とくに、次の型定義ファイルをいれたときに、更新が設定ファイルのリロードに依存していて、結構頻繁にこれを叩くことになる。

外部型定義ファイルの導入

Flow は型定義集積場所として https://github.com/flowtype/flow-typed がある。ベーシックなものはここにある。が、如何せん数は足りない。

npm install -g flow-typedで入る flow-typed というインストールコマンドがあり、node_modules から flow-typed の型ファイルを自動で解決するが、すべてのパスに対し、大量のデッドコードを生成するので、はっきり言って邪魔。

じゃあどうしていることが多いかというと、 「flow-typed install redux@3.x.x」で指定したものだけ入れるか、「手作業で flow-typed からコピペする」ということをしていることが多い。

大抵こういうディレクトリになっている。自分の手元の例。

flow-typed/
├── npm
│   ├── react-redux_v5.x.x.js
│   ├── recompose_v0.21.x.js
│   ├── redux-saga_v0.13.x.js
│   └── redux_v3.x.x.js
└── redux-helper.js

recompose_v0.21.x.js は、PRはでてるがマージされてないので、自分でとってきた。

redux-helperは自分で書いたreduxのラッパーでflow-typedのではないので、npm の下にはない。

この flow-typed ディレクトリを libs 以下に指定すると、その型が読み込まれる。

追記: hitode909さんにTwitterで指摘されたが、今のflowは暗黙に flow-typed を読み込んでるので、今は下の設定は不要だった。

.flowconfig
[libs]
./flow-typed

typescript の typings などと違って、バンドル機構が発達してない & 安定していないので、定義を .gitignore で運用するといったことは現実的ではない。

実際、これで困るかというと、その開発環境においてのフレームワークのコアとなるライブラリさえ型があれば、意外と困らなかったりする。そもそもTSでも全部のAPIの型が揃ってないと開発できない、というレベルの運用は、今のJSではあまり現実的ではない。

Flowの型定義ファイルの読み方

たとえば redux の型定義の冒頭はこうなっている。

// flow-typed signature: 7f1a115f75043c44385071ea3f33c586
// flow-typed version: 358375125e/redux_v3.x.x/flow_>=v0.33.x

declare module 'redux' {

  /*

    S = State
    A = Action

  */

  declare type Dispatch<A: { type: $Subtype<string> }> = (action: A) => A;

  declare type MiddlewareAPI<S, A> = {
    dispatch: Dispatch<A>;
    getState(): S;
  };
  ...

declare module 'xxx' が、import されるときの参照に型を付ける目印になる。これを参考にすれば、自分である程度足りない型を追加することができる。

eslint

https://github.com/gajus/eslint-plugin-flowtype を使う。おそらく パーサに babel-eslint も必要だろう。

↑のREADMEもあるが、とりあえず reccommended などを使うといいだろう。

{
  "extends": [
    "plugin:flowtype/recommended"
  ],
  "plugins": [
    "flowtype"
  ]
}

CSS Modules と一緒に使う

自分はあまり好きではないが、css-modules と一緒に使う場合、拡張子.css が解決できない、と言われるだろう。認識できる拡張子を増やして対応する。

.flowconfig
[options]
module.file_ext=.js
module.file_ext=.css
module.file_ext=.scss

これでとりあえず パス解決はできる。importした際の推論結果は any。 js/jsxを区別してる場合も同じようにする。

型の import / export

ここだけ Flow の機能的な独自拡張に含まれる。

foo.js
/* @flow */
export type Foo = {}

これの読み込みは次のようになる

bar.js
/* @flow */
import type { Foo } from './foo'

export default type ... はできない。今のところ。

flow-runtime

静的解析では追いつかないときに、ランタイムチェッカもいれると便利なときがある。

https://codemix.github.io/flow-runtime/

yarn add flow-runtime babel-plugin-flow-runtime -D

ここで注意すべきは、presets の flow を外して、flow-runtime をいれることだ。strip-flow-types で型の消去のみが行われてしまう。

.babelrc
{
  "presets": ["es2015"],
  "plugins": [["flow-runtime", {"assert": true}]]
}

正直、まだ挙動が怪しいライブラリなので、試しにいれて、うざかったら消すといいと思う。あるいはテスト時のみ NODE_ENV で有効化するか。

以上

TODO: 参考リンクを足す