背景
JavaScript はオブジェクトの構造とかコンパクトでプラットフォーム非依存な仕様など、面白いと思う反面、
'' < 1
が true になってしまうなど割とつらい仕様もあるので、大々的には入れたくないなぁと以前から感じていたのですが、ここ数年は JavaScript に型を入れるというのは AngularJS 2 の TypeScript 採用、Facebook の Flow など話題だし、ES2015 にはなんとかえっちらおっちら追いつけそうなので、いっちょ Flow でも試してみることにしました。
※ 私は Ruby を使っていますが、Ruby の場合は型が欲しいと思うことはありませんでした。Ruby にも静的型はないじゃないかと思うかもしれませんが、上のような謎挙動はないので、全然意味が違うということは添えておきたいと思います。
実際のところは直接書いたコードではなく簡易DSLのようなナニカで上の '' < 1 を踏み抜いてしまったことにより型の必要性を感じているので、これを入れればすぐ問題は解決、というわけにはいきませんが、興味が湧いちゃったのでまずはこれをやってみましょう。
判断基準
なぜ Flow なのか。以下を読むべし。
このエントリでは条件が整えば TypeScipt でよい、という判断ですが、自分の場合は、ここ何年かの JavaScript の情勢や ES のリリース方針を見ると、本流である Ecmascript が常に漸進的に更新されていくことがもはや明確であり、正直言うと本家以外の ES の亜流には手を出したくないというのが本音であり、TypeScript も MS がいつまで本気でやるかも分からないという一抹の不安はやはり残っています。1
その点 Flow は type 情報を strip すればただの Ecmascript というところが安心。いま欲しい何かを満たしつつ言語仕様そのものは標準を維持できるので長期的に見てもメリットが大きいと思います。2
Flow: A Static Type Checker for JavaScript
ということで Flowtype 行ってみよー。
準備
すでに babel, eslint は使えているものとします。
$ yarn add --dev \
babel-eslint \
babel-preset-flow \
babel-plugin-transform-class-properties \
eslint-plugin-flowtype \
flow-bin
以下、追加変更する設定ファイルの部分。
..eslintrc.json
{
"parser": "babel-eslint"
}
type の書かれたコードは通常の Ecmascript とは異なるので、eslint の parser を babel を通したものにします。
package.json
"scripts": {
"flow": "flow check",
"flow-stop": "flow stop"
}
..babelrc
{
"plugins": ["transform-class-properties"],
"presets": ["flow"]
}
で、babel を通すと flow な preset が適用され、余計な情報はカットされます。
yarn flow -- init
として .flowconfig を以下のような感じにしてみました。
[ignore]
.*/doc/.*
.*/dist/.*
.*/node_modules/.*
[include]
[libs]
[options]
log.file=flow.log
[lints]
ほとんど何も設定してません!
気づかなかったけど気をつけた方がよいこと
flow は daemon として動く。そしてそのまま居座る。
0.49.1 で試し直したら終了した時点でプロセスが消えたので、これが正しい動作なのかもしれません!!
やってみた
@flow
コメントに @flow を書くと有効というか検査対象になります。どこでもよいわけではないようで、できるだけ1行目に書いた方がよさそうです。
// @flow
でも
/* @flow */
でも
/**
* @flow
*/
でもいいけど、とにかくいちばん上に置くのが肝要なようです。
ちなみに、いちいち @flow を書くのが面倒な場合は
flow check --all
とすればよいようです。ただし、テストコードでこれが有効になると割と面倒なことになりそうです。
型を書かなくても error になる
console.log('' < 1)
^^ string. This type cannot be compared to
console.log('' < 1)
^ number
リテラルを解釈して型が違うものを比較している場合にはそりゃーダメだよと教えてくれます。いきなり便利!
引数に型を書く
基本的には変数名の後ろに :<type> を付ける形。イマドキこの形が多いと聞いたような? でもよく分かっていない。
function foo(abc :string) {}
と type 情報を付けておくと、
foo(1)
呼び出しが書かれている場合、flow を実行すると以下のように怒られます。
: foo(1)
^ number. This type is incompatible with the expected param type of
: function foo(abc :string) {}
^^^^^^ string
おおおお。これが自分的にはいちばん嬉しいかも。
戻り値に型を書く
function foo() :string {}
となっていたら number を返そうとしてると怒られるわけですな。
ローカル変数に型を書く
var foo :string = 1
は
var foo :string = 1
^ number. This type is incompatible with
var foo :string = 1
^^^^^^ string
みたいに怒られますが、この例のように書いてしまうことはまずないだろうし、何かの設計をミスって長寿命な変数を使ってしまった場合に有効なのかな?
プロパティに型を書く
これがドキュメントを見渡しても分からず、
this.foo :string = 1
と書いてみたところ
this.foo :string = 1
^ Unexpected token :
型のチェックではなく文法的にアウトという表示になってしまう。これの正解は上でしなっとインストールしてあった
Class properties transform · Babel
を使って
class Foo {
bar :string = 1
}
のような形で書くことで解決です。
フィールド定義は 2017-07 時点では Stage 2 ( Draft )
今回利用した機能は 2017-07 時点で Stage 2 ( Draft ) の内容らしく、ES2015 の範囲外の機能となる
tc39/proposal-class-fields: Orthogonally-informed combination of public and private fields proposals
の Field declarations に書かれている内容になります。要は
class Foo {
x = 0;
に対して type 情報を追加すると
class Foo {
x :number = 0;
になるということなのですが、そもそもこの書き方が ES2015 の範囲外なので、Flow を利用してプロパティ / フィールドに type 情報を追加したい場合、Babel に
babel-plugin-transform-class-properties
を追加することが必要になります。
実際にはこんなアホな初期化ミスはないと思いますが、ローカル変数と違ってプロパティの場合はどこからアクセスされるか分からない長寿命な変数の一種と言ってよいので、type の恩恵はおおいにありそうです。
ちなみに
なんでこの書き方に思い至ったかというと、
TypeScript in 5 minutes · TypeScript
を見て気づいたのでした。
結局 TypeScript の情報もチェックしてるよ!
あとはIDE的なものの対応がよく分からない
自分の場合は Emacs で読み書きして別途 flow や lint を動かすので構わないんだけど、IDE にこの辺の機能が組み込みの場合にどう扱ったらいいかは分からないです。実際 VS Code で開くと *.ts の場合は自動的に TypeScript と判定されて問題ないのですが、*.js の場合は type が書いてあると怒られます。
自動化の部分ではクリアできてるとは言え、いちいちコマンドを呼ばなくてよい、表示も統合されてて見やすいといった IDE のメリットがこのままではスポイルされてしまいます。
…と思ったら
Flow Language Support - Visual Studio Marketplace
を見ると
"javascript.validate.enable": false
するのが Known Issue になってた。うーーーーーーん。イマイチだなぁ。
ESLint - Visual Studio Marketplace
も、こまごま設定する必要があるんですな。この辺もバージョン管理にぶちこめるといいのかもしれないけど、それはそれでアレ。
まぁ今後の課題ということで。