TypescriptとFlowどちらを採用するべきか

僕は普段フロントエンドフレームワークと共にTypescriptを使って開発をしています。

ReactやVue.jsの内部実装にはFlowが採用されていて、AngularやRxJSはTypescriptを採用しています。
今後開発を進めていく上で両者はどのような開発現場でメリットが最大化されるのか調査していみたいと思います。
TypescriptをベースにFlowにはどんな機能が盛り込まれているのか、または不足しているのかを書いていきます。

変数の型宣言後の再代入

Typescriptではvarletで宣言した変数を適当な値で初期化すると、型宣言がされていなくても型推論により以降の再代入は初期化時の値の型に束縛されます。
Flowでは、初期化時に型宣言をしていない場合は以降の再代入はどんな型でも代入可能です。

Typescript

let age = 21;
age = '22' // => Error

let name: string = 'Anonymous';
name = 21  // => Error

Flow

// @flow
let age = 21;
age = '22' // => OK

let name: string = 'Anonymous';
name = 21  // => Error

null型 / undefined型の扱い

null, undefinedはTypescriptではどんな型で束縛された変数にでも代入可能です。
Flowは厳密に精査してくれるので、例えばstringで宣言した変数にはnullundefinedも代入できません。
逆も同様でnull宣言の変数に対してstringの値は許容しません。

Typescript

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}
new Person(null);      // => OK
new Person(undefined); // => OK

Flow

// @flow
class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}
new Person(null);      // => Error: Cannot call Person with null bound to name because null is incompatible with string.
new Person(undefined); // => Error: Cannot call Person with undefined bound to name because undefined is incompatible with string.

Typescriptはnullを許容し、Flowではnullを許容しないということです。
しかし、Typescript2.0からはtsconfig.jsonコンパイラオプションを与えるとnullを許容しない設定も可能です。
ここで紹介したのはtsc(Typescript compiler)にオプションを与えていない場合になります。
Flowでもnullを許容して(nullable)宣言をしたい場合?キーワードを付けてあげればいいです。

// @flow
function nullableFunction(value: ?number) {
  if (value != null) {
    /* ...... */
  } else {
    /* ...... */
  }
}

nullableFunction(42);         // => OK
nullableFunction();           // => OK
nullableFunction(undefined);  // => OK
nullableFunction(null);       // => OK 
nullableFunction("42");       // => Error

引数として受け取る変数がnull許容(nullable)の場合は、多くの場合上記のように関数ブロック内でnullチェックすることになると思います。

型推論について

上述した通りTypescriptでは変数宣言時に初期化する場合、初期化した際の値の型を推論します。
Flowの変数宣言時は型推論は行われませんでしたが、関数やクラス内のメソッドの内部で行われる演算処理に対しては引数を厳密にチェックします。
先ほどのPersonクラスの例のconstructor内に1行だけ演算処理を追加しました。

Typescript

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
    this.name * 1;
  }
}
new Person(null); // OK

Flow

// @flow
class Person {
    name;
    constructor(name) { // => Cannot add this.name and 1 because mixed could either behave like a string or like a number.
      this.name = name;
      this.name * 1;  // null*1は妥当な演算ではない
    }
}
new Person(null); // Error

anyにしてしまえばコンパイルエラーは吐かれませんが、それ以外の場合その変数を参照している箇所すべてをコンパイラがチェックしてくれます。
constructorではなく新たにメソッドを定義してその中で妥当ではない演算処理等をした場合もしっかりエラーを吐いてくれます。

結論

普段Typescriptを使用していてる僕がFlowを魅力的だと思えるのは型推論のあたりでしょうか。
Flowは型定義ファイルをflow-typedというリポジトリで扱っています。

github.com

実際のところFlowをサポートしているライブラリはまだまだ現時点では少ないようです。
FlowにはTypescriptにはない多くの言語機能が盛り込まれていて、他のライブラリには依存しないようなOSS開発に向いているような印象でした。
この記事では紹介してないOpaque TypeType Casting Expressionsという機能もTypescriptにはない便利そうな機能です。

Typescriptはサポートしてくれてるライブラリも多いので一番大きな決めてになっているのが現状です。
まだ感触程度でしかFlowについて知れていませんが、Flowの型推論は非常に強力で気に入りました。
今後もしサポートが手厚くなりそうならすぐにでも乗り移りたいくらいです。