はじめに
「JavaScriptに型を付けるならTypeScript一択」と思っていませんか。実はMeta(旧Facebook)は自社の膨大なJavaScript・Reactコードベースに対して、TypeScriptではなく自社製の静的型チェッカー「Flow」を使い続けています。
TypeScriptがデファクトスタンダードになった今、なぜMetaはFlowを手放さないのでしょうか。理由は、TypeScriptが見逃しがちな実行時エラーの芽をFlowが厳格に検出してくれること、そしてReactコンポーネントを第一級の構文として扱える点にあります。この記事では、Flowの特徴からインストール、実践的な使い方までを見ていきます。
Flowとは
Flowは、Meta(Facebook)が開発しているJavaScript向けの静的型チェッカーです。TypeScriptのようにJavaScriptを別言語にコンパイルするのではなく、型注釈を除去するだけで素のJavaScriptとして実行できる「型付き方言」という立ち位置を取っています。
2014年の登場以来、React本体の型チェックにも使われてきた歴史があり、現在もMeta社内で数百万ファイル規模のコードベースを支えています。近年はTypeScriptと構文が大きく歩み寄り、keyofやreadonly、条件型・マップ型といった機能を共有する一方で、Reactに特化した独自機能を伸ばしているのが特徴です。
主な特徴
- TypeScript互換の構文 -
keyof、readonlyプロパティ、unknown、インデックスアクセス型、条件型・マップ型など、TypeScriptを知っていればほぼそのまま読み書きできます - Reactファーストクラスサポート -
component構文やrenders型がFlowの文法に組み込まれており、propsやコンポーネントの合成契約をより直感的に型付けできます match式によるパターンマッチング - 網羅性チェック付きの分岐処理が言語機能として用意されています- デフォルトで厳密なオブジェクト型 - オブジェクト型は既定で「正確(exact)」に扱われ、余分なプロパティの混入を検出します
- 安全性(soundness)重視の型推論 - TypeScriptが許容してしまう不正な
thisコンテキストや危険なメソッド抽出など、実行時にバグにつながりやすいパターンを厳しくチェックします
インストール
Flowはプロジェクトごとにローカルインストールするのが基本です。npmパッケージ名はflow-binです。
npm install --save-dev flow-bin
package.jsonにスクリプトを追加しておくと呼び出しが簡単になります。
{
"scripts": {
"flow": "flow"
}
}
プロジェクトルートで初期化コマンドを実行すると、設定ファイル.flowconfigが生成されます。
npm run flow init
基本的な使い方
型を付けたいファイルの先頭に// @flowというプラグマコメントを置くと、そのファイルがFlowのチェック対象になります。
// @flow
function greet(name: string): string {
return `Hello, ${name}!`;
}
greet('Flow'); // OK
greet(42); // Error: numberはstringと互換性がありません
型チェックはバックグラウンドプロセスとして動き続け、ファイルを保存するたびに差分だけを高速に再チェックします。
npm run flow
Flowは型注釈をランタイムに一切残しません。ブラウザやNode.jsで実行する前に、Babelの@babel/preset-flowやflow-remove-typesパッケージで型注釈を取り除く必要があります。
npx flow-remove-types src/ -d dist/
こうした型チェックとコード整形はいずれもNode.js上のCLIコマンドとして実行するものなので、ブラウザ上でその場で試せる実行サンプルは用意していません。以下のコード例はいずれも手元のプロジェクトにコピーしてnpm run flowを実行することで挙動を確認できます。
実践的なユースケース
component構文でReactのpropsを型付けする
FlowはReactコンポーネントを言語レベルでサポートしており、componentキーワードでpropsを名前付き引数として直接宣言できます。旧来のReact.Nodeを返す関数コンポーネントに型を後付けするより、意図が読み取りやすくなります。
// @flow
import * as React from 'react';
component Button(label: string, onClick: () => void, disabled?: boolean) {
return (
<button onClick={onClick} disabled={disabled ?? false}>
{label}
</button>
);
}
// <Button label="送信" /> はonClickが必須のためFlowがエラーを検出する
renders型でコンポーネントの合成契約を宣言する
デザインシステムやラッパーコンポーネントを作るとき、「このコンポーネントは最終的に何を描画するのか」を型として表明できるのがFlowのrenders型です。ラップされる側の実装が変わっても、契約に違反すればFlowが検出します。
// @flow
component Card(children: React.Node): renders React.Node {
return <div className="card">{children}</div>;
}
component FeatureCard(title: string): renders Card {
return <Card><h3>{title}</h3></Card>;
}
match式で網羅的な分岐を書く
状態遷移やAPIレスポンスの種類分けなど、パターンごとに処理を分けたい場面ではmatch式が便利です。すべてのケースを網羅していない場合、Flowがコンパイル時に指摘してくれます。
// @flow
type Status = 'idle' | 'loading' | 'success' | 'error';
function label(status: Status): string {
return match (status) {
'idle': '待機中',
'loading': '読み込み中',
'success': '完了',
'error': 'エラー',
};
}
まとめ
Flowは「TypeScriptの代替」というより、「TypeScriptより一歩厳格な安全性と、Reactに特化した構文を求めるチーム」のための選択肢です。component構文やrenders型、match式のように、Reactを書くことを前提に設計された機能はTypeScriptにはない強みといえます。
すでにTypeScriptを使っているチームがすぐに乗り換える必要はありませんが、Metaが今も本番コードで使い続けている理由を知っておくと、型システムを選ぶときの視野が広がるはずです。興味を持った方は、まず小さなコンポーネントに// @flowを付けるところから試してみてください。
