Flowtype 導入メモ
結構な数のつまづきがあったのでメモしておきます。
なお、Flow は現在も活発に開発中のため、そこそこカジュアルにBreaking Changeする可能性があります。
この記事はあくまでつまづきやすかったポイントを残すにとどめているので、基本は公式のドキュメントを参考にすることをお勧めします。
つまづきポイント
- Nuclide の設定がややこしい
- Flow サーバーがそこそこ不安定
- Flowのコードは2種類ある
- FlowのエラーとESLintのエラーがある
- class-fields っぽい書き方とオブジェクトリテラルっぽい書き方がある
- ドキュメントやエラーメッセージに出て来る英語が難しい
- ドキュメントの擬似コード
- 正確な型を表現することの難しさ
一つ一つの難易度はそれほど高くなくても、二つ以上のつまづきが同時に起こる事で対処が難しくなる。
問題を切り分けるのが大事。
Nuclide の設定がややこしい
とにかく設定項目が多いので必要ない機能は全部切る。
この記事を参考に設定しました。
Nuclide というのは Atom のプラグインで、Facebook スタックの開発環境をカバーする全部入りの IDE 的なもの(だと思っている)。
Flow 以外にも Hack、Swift などのサポートもある。1
config.cson
を置いときます、Swift 関連も有効にしてる。
その他のエディタサポートについてはこんなものがあるようです。
Flow サーバーがそこそこ不安定
Flow がコードを検証する時、毎回全てのコードを読み込むわけじゃなくて、裏でサーバーを立ててキャッシュし、二回目以降のチェックではそれを利用するような仕組みになっているらしい。
注意点として、Flowは型情報の収集にバックグラウンドでサーバを走らせるのだが、.flowconfig をファイルを書き換えた際の検知がそこそこの確率で失敗している。困ったら flow stop; flow と叩いていることが多い。 flow restart もうまく動いていないことが多い。経験上。
とくに、次の型定義ファイルをいれたときに、更新が設定ファイルのリロードに依存していて、結構頻繁にこれを叩くことになる。
これホントその通りで、感じが掴めるまではサーバーが変更後のコードを検証してくれてるかどうか注意したほうがいい。
やってるうちにだんだんサーバーの気持ちが分かるようになってくるという謎の技術でカバーするのがまた良くない。
一番確実なのは flow check
を実行すること。 経験上。
Flowのコードは2種類ある
通常の JS コードに型注釈を付けていく、いわゆる普通のシンタックスの他に、declare
キーワードを使ったライブラリの型定義のためのシンタックスがある。
少しややこしいのは、この declare
キーワードを使ったシンタックスは、通常のコードに混ぜて使うことも出来る、という点。
しかもこの機能、ドキュメントからいつの間にか消えているので、なぜエラーにならないのかもはや知る術がない。2
何かの拍子に書いた際にエラーにならなくて、エディタの動作を疑ったりした事があったので、とりあえずこういうシンタックスがあるということを知っておく。慌てない。
FlowのエラーとESLintのエラーがある
自分は Atom の Nuculide と linter-eslint を使ってそれぞれエラーがあればエディタ内に表示されるようにして使っている。
ここで気を付けないといけないのはエラーを出しているのが Flow なのか ESLint なのかを意識するということ。
具体的には ESLint のこのエラーの時に Flow のエラーだと勘違いしてハマった。
そんなところでつまづかねえよと思われるかもしれないが、Flow のエラーを修正してる途中でポッと ESLint のエラーを吐かれたりすると、頭が Flow の世界に入ってしまっているので、なぜエラーになるのか分からずしばらく悩んでしまった。
もちろん、エラーメッセージをちゃんと読めっていう話なんだけども。
これに加えて ESLint に Flow 向けのプラグインとルールを設定しておくと良い。
- flowtype + eslint 環境で eslint の no-undef で declare された型の型アノテーションの検出を無視する
- eslint と flow で import と import type で no-duplicate-imports の警告が出るのを消す
最近本体にも Lint の機能が入ったのでこれも設定するとなお良い。
Class Fields っぽい書き方とオブジェクトリテラルっぽい書き方がある
Class Fileds は class ブロック直下でプロパティを初期化できるというもの。
Object Types と Interface Types の定義の時に、各プロパティーのデリミタを ,
にするか ;
にするかという問題。
結論から言うとどちらも同じだそうです。ただし Object Types については、セミコロンは後方互換性のためでカンマ推奨。
Interface Types についてはよく分かんないっす。class の interface なのか、単純な map の interface なのか文脈で使い分ければ良いのかな。
Note: Previously object types used semicolons ; for splitting name-value pairs. While the syntax is still valid, you should use commas ,.
以下のように4つの型全てに互換性がある。
type SemicolonObject = { foo: number; bar: boolean; baz: string; }; type CommaObject = { foo: number, bar: boolean, baz: string, }; interface SemicolonInterface { foo: number; bar: boolean; baz: string; } interface CommaInterface { foo: number, bar: boolean, baz: string, } const so: SemicolonObject = { foo: 1, bar: true, baz: 'three', }; const si: SemicolonInterface = so; // OK const co: CommaObject = si; // OK const ci: CommaInterface = co; // OK
じゃあ Interface と Object の違いは何かという話なんだけど、ドキュメントを見るとまあいろいろ違う。
自分は以下のように使い分けてる。
- Interface Types
- class の interface を定義したい
- extends したい
- static field 使いたい
- Object Types
- それ以外の時
ドキュメントやエラーメッセージに出て来る英語が難しい
前提として自分は英語が得意じゃないです。平易な英語なら読めるけどちょっと難しいのだと解釈が合ってるか自信ない。
あとは、型についての知識とか専門用語についても Flow を触り始めてから学んだことが多い。
そのせいも大いにあるが、ドキュメントやエラーメッセージに出てくる単語や言い回しは見慣れないものが多く、独特の雰囲気を感じる。
たとえば Lint Rule の中の一つで sketchy-null
というのがあるんだけど、どういうルールなのかパッと連想できるだろうか。
これは if (value) { /* ... */ }
のような甘い null チェックを指摘してくれるルールなんだけど、sketchy
っていう表現は最初なんだか分からなかった。
英語分かる人にとってはむしろ直感的だったり、気の利いた言い回しだったりするんだろうなとは思う。
それから、専門用語。
静的型付き言語の世界では一般的な言葉なのかもしれないけど、variance、variant、covariant、invariant、contravariant みたいな言葉がドキュメントとかエラーメッセージでガンガン出てくる。
英語の壁と型の壁、二重の壁が立ちはだかる。というと大げさかもしれないが、チームで導入するならそういう心理的障壁は考慮する必要があるかもしれない。
ドキュメントの擬似コード
Flow のドキュメントは Flow が型をどう扱うか、ということの説明に結構多くの部分を割いている。
それ自体はすごい助かるし勉強になるんだけど、時々説明のために擬似コードを使うことがあって、前後の文脈を読まずにサンプルコードだけを追うと訳が分からなくなる。
以下の場所で使われている。
正確な型を表現することの難しさ
単純なコードの場合はいい。
コードが少しでも複雑になると、自分の頭の中では型が解決できていても Flow がエラーを吐く、という場面がけっこう出てくる。
このエラーを取り除くには、自分の頭の中の型情報を正確に Flow に伝えてあげる必要がある。
型情報を正確に Flow に伝えるには、Flow を用いた型の表現をしっかりと身に着けないといけない。
Flow 自体には高い表現力があるが、それだけにそれを正確に使いこなすのが非常に難しい。
複雑な表現はミスが入り込む余地も大きくなるので、慣れるまでは正確な型情報を一発で書けるということは中々無い。3
中途半端な理解で無理して型を表現しようとすると、型情報が間違っていたりするだけでなく、間違っているかどうかを判断することも難しくなるので、どんどん本筋と関係ないところで時間を奪われることになる。
段階的な導入は可能?
Flowtype の一つのメリットとして、段階的に導入できるという部分が挙げられることが多いが、果たして本当にそうだろうか?
自分が思うに、それはある意味では正しく、ある部分では誤解を与えているような気がする。これはコマーシャル的な意味でおそらく意図的にそうしているのかもしれない。
ここで言う段階的というのは、あくまで Flow を適用する範囲の話であって、学習曲線がなだらかで覚えやすいから導入しやすい、という話では無い。
ダイジョブ、ダイジョブ、使いながらおぼえればいいからではないのだ。
上でも書いたとおり、Flow で思った通りに型を表現出来るようになるまでには結構時間がかかる。
そんな状態で既存コードに型を付けていこうとしても、結構なんもできない。ある程度 Flow に精通して型の表現力を身に付けていないと、ただただ any
まみれで何が嬉しいのかさっぱり分からないコードになってしまう。
初めから Flow で書くほうが、知っていることしか書けない分むしろ楽。
導入は段階的でも、学習コストは初めにしっかり払う必要がある。
で Flow どうなの?
上で挙げたようなつまづきを乗り越え、晴れて型の恩恵に預かれるようになった暁には、たしかに Flow の型推論は賢いと感じたし、個人的にはこの先も触っていきたいと感じた。
ただ Breaking Change がまだまだあるかもしれないし、今直ぐギョウミーなコードに導入するのはリスキーかもしれない。
チームで導入するなら、ある程度 Flow の習熟度や型の知識などを揃えられないと現実的じゃないと思ってる。
勉強したり共有したりする時間とメンバーが確保できるならやってみたい気持ちはある。
最近 Angular を触り始めた影響で TypeScript にも入門したが、めちゃくちゃ簡単に導入できたような印象がある。
これはもちろん Flow で先につまづいておいたポイントを回避できた部分も大きいが、VSCodeや圧倒的な型定義ファイルの量(と質?)が物を言ったという感じがする。
TypeScript と Flow は共通点も多いので、片方で得られたノウハウはもう片方でもある程度使えると思う。
好きな方を勉強しておけばいざという時にはそれほど困らないような気がしてる。
それにバージョンアップを重ねるごとに違いも無くなっていってる気がするし。
そんなわけで Flow はそういうことも引っくるめて面白いので、これからも適当に watch していくと思う。
【おまけ】ドキュメントから消えた機能
Mixins
今ではもう使えなくなっている。うそ。まだ使えた。4
// You can mixin more than one class declare class MyClass extends Child mixins MixinA, MixinB {} declare class MixinA { a: number; b: number; } // Mixing in MixinB will NOT mix in MixinBase declare class MixinB extends MixinBase {} declare class MixinBase { c: number; } declare class Child extends Base { a: string; c: string; } declare class Base { b: string; } var c = new MyClass(); (c.a: number); // Both Child and MixinA provide `a`, so MixinA wins (c.b: number); // The same principle holds for `b`, which Child inherits (c.c: string); // mixins does not copy inherited properties, // so `c` comes from Child
Inline declarations in regular code
Qiita の記事でサンプルコードに使われてたりして、なるほどこういう時には便利だなと思ったことがあった。
declare class List<T> { map<U>(f: (x: T) => U): List<U>; } declare function foo(n: number): string; function fooList(ns: List<number>): List<string> { return ns.map(foo); }