動的型付き言語への静的型チェックの導入に失敗した話
動的型付き言語である JavaScript (node.js) で開発・運営されているプロジェクトに静的型チェック機能(flow)を導入しようとして失敗した話を共有します。
なぜこの記事を書こうと思ったのか?
- ある程度の数の人が同じことを検討しているという確信があります
- 成功事例は真似しても同じように成功するとは限らないが、失敗事例は同じことをすると大体失敗するのでより価値があります
- 率直に言うと Advent Calendar だと他の場所に比べて失敗談も書きやすいと考えました
Flow とは何か?
JavaScript に静的型チェックできるツール(ライブラリ)のことです。
メリット
- 実行前に型チェックを行うことで安心感を得られる
- 不具合を減らすことができる
- 意図しない変数への代入や変数名の間違い、関数への渡す引数の間違いを減らせる
- 仕様上の問題をあぶり出すことができる
- 不必要な変数の使い回しや再代入、大きすぎる自由度に制限を加えられる
デメリット
- 変数や関数に型を記述する手間が増える
- 変数や関数の型を気にした設計が必要になる
背景
Flow 導入移行時(半年くらい前)の状況は以下のとおりです。
- プロジェクトに関わるエンジニアの人数: 20人程度
- 担当していたプロジェクト: リリース後2年の node.js で書かれた web サービスのアプリケーション開発
- 筆者のスペック:
- 開発歴10年以上(Python,C#,Haskell など、node.js での開発歴は浅い)
- 会社およびプロジェクト参加後半年
- 開発サイクル:
- PullRequest、コードレビューあり
- 自動化された CI/CD 導入済み
- ユニットテスト、e2eテストあり
開発環境に対する不満
- Vue.js のバージョンが複数混在している
- ユニットテストという名前の機能テストの実行速度が激しく遅い
- 開発環境とCI環境が違いすぎてCIで出たエラーがローカルで再現しない
上記の問題により開発とデバッグがしづらい状況でした。
静的型チェックを導入すれば状況が改善するかもしれないと考えて flow の導入を試すことにしました。
flow 導入
基本方針
-
Comment Types 方式で記述する
let hoge/*: string */ = 'hogestr';
-
flow-typed で依存ライブラリに対応する
TypeScript における DefinitelyTyped のようなもの - decl ファイルに global 変数や内部ライブラリの定義を記述する
運用ルール & Tips
- 極力
any
を使わない (any
を使わないとflowのエラーが解消できない時は設計を疑うべきかも)
使用を考える順序としては、string | number
のUnion Type
>mixed
>any
- nullの可能性があるものは積極的にプレフィックスに?をつける(例:
/*: ?string */
)
→nullチェックの見落としが減る - メソッドの引数には必ず型をつける(つけないとデフォルトでエラーになる)
- メソッドの戻り値の型はできればつける(voidはつけない)
- letでの定義には型をつける
- constで定義したメソッドでは無いものにもなるべく型をつける
ただしスコープの小さいメソッド内の変数で明らかに型宣言が無意味なら不要 - node_modules 以下は [ignore] に設定し、かつ [options] >
module.system.node.resolve_dirname
に書く - どうしても flow に対応できないライブラリの記述を避けるために以下の設定を options に導入する
.flowconfig
suppress_comment= \\(.\\|\n\\)*\\flow-disable-line
.js
some-disable-line```
### 導入手順
1. flow を開発環境と CI のテストタスクに組み込む
2. グローバル変数を decl に型定義する
3. 1つのファイルに flow を適用する
4. 依存ライブラリの一部に flow-typed を適用する
5. 別のファイルに対して flow を適用する(3に戻る)
## 失敗の原因
一言で言うと、導入コストの見積が甘かったのが失敗の原因です。
### 原因1: 仕様の問題や不具合で想定以上の時間がかかった
想定し難い仕様や下記のハマったポイントに書いたような謎の問題の解決に時間がかかってしまいました。
### 原因2: flow-typedのカバレッジ率や型の適用度が想定より低かった
flow-typed が対応していないライブラリも多く、かつ対応していても型の厳密度が甘くてほとんど型チェックの意味がないような型定義も多くありました。
### 原因3: 想定よりも高まったコストを上回るほどの導入によるメリットを見いだせなかった
困難を乗り越えて上記の2つの問題をたとえ解決できたとしても、そのために支払うコストをかけるだけのメリットは見いだせませんでした。
## どうすればよかったのか?
最初の flow テストを実施した段階で 6000 個以上のエラーが出た瞬間に撤退すべきでした。
損切り大事。
## ハマったポイント
- nullを許可してオブジェクトを定義しておいた時、nullチェックをしてもメソッドの最終行でそのオブジェクトメンバを参照するとエラーが出ます
https://github.com/facebook/flow/issues/3786
```js
let hoge/*: ?Object */ = null;
hoge.say = () => {
console.log("say");
}
const fuga = () => {
if (hoge.say) {
// 何かしたあとに
hoge.say(); // nullチェックしていても、チェックの直近でないとerrorになる
}
};
- オブジェクトの入った配列でArray.sortするときにbooleanのみを返しても動くが、flowではnumberを返す関数を求められます
someArray.sort(function(a/*: {[string]: any} */, b/*: {[string]: any} */) {
return a.point < b.point;
});
flow が受け付けるのは下記のような関数のみ
someArray.sort(function(a/*: {[string]: any} */, b/*: {[string]: any} */) {
if (a.point < b.point) {
return -1;
};
if (a.point > b.point) {
return 1;
};
return 0;
});
補足
- 上記の問題があったのは flow 導入開始時点であり、執筆時には上記の問題の一部は改善されています
まとめ
flow を途中から入れるのは極めて困難なので、新規の開発を始める時に最初から導入すべきです。
flow の導入に失敗した原因は flow 自体の問題よりも導入プロセスの問題にあります。
動的型付き言語に静的型チェックをする flow の性質上、開発の途中から導入するのには非常にコストがかかります。