FlowtypeやTypeScriptは静的解析によって事前に型違反を検知することができる。JavaScriptは動的型付けの言語であり、本来はランタイムにしか型が出現しない。
FlowtypeとTypeScript、ともに「それ自身がランタイムではない」というのが特徴であり、一種のLintツールだと言うことができる。ランタイムではないがゆえに、嘘の事前条件を与えることでそれらを騙すことができるし、自らに有利な制約を追加できるという柔軟性を持つ。
JavaScriptの現実においての型
例を出そう。
type MyUtil = { foo(v: string): number; };
const util: MyUtil = new HogeUtil();
util.foo(1) //=> type error
HogeUtil は何かしらのユーティリティ関数の詰め合わせだが、fooにしか興味がないことをこのコンテキストでは明示できる。
TypeScript で implicit any
を許容したり、Flowtype のオプションで module.ignore_non_literal_requires=true
とすると、require/importしたものがanyとなり、anyのまま扱ってもいいが、any を何かしらの型へダウンキャストすることで安全性を高めることができる。
また、逆に any へのアップキャストは許可されているので、どうしてもライブラリの用途と自分の用途が噛み合わない時に any へのキャストを行うことがある。
const v = somithingPolymorphicFunc();
util.bar(v); // 何かしらの理由で通らない
const tmp: any = v;
util.bar(tmp); // 無理やり通す
これは危険な操作に見えるが、そもそも後付の型システムで、型を考慮せずに書かれた第三者の型定義を使っていた場合、APIが網羅されていない時の脱出ハッチとして使うことが多い。使うライブラリ次第では、こんなコードにならざるを得ない。
一つの選択肢として、バニラな状態からはじめて、段階的に型を付与していく、というのもある。
interface Underscore {
map<T, U>(xs: T[], f: (t: T) => U): U[];
};
declare var _: Underscore;
これで、レガシーな環境のUnderscoreの map だけ使ってる場合に、その違反を検知できる。必要に応じてライブラリのインターフェースを自分で決めながら開発を進めていくスタイル。Flowtypeは外部の型定義ファイルがTypeScriptに比べて劣るので、こうならざるを得ない。
経験上、TypeScriptでも型定義が自分の用途と噛み合わないので、自分で再定義することは頻繁に発生した。
後付のための柔軟な型システム
そもそも、今までのJavaScriptは型があることを前提にしていなかったわけで、メタにそれらを扱う必要があるので Flowtype/TypeScript は、ともに柔軟なNullable記法やUnionTypeなどの型を持つ。
const x: "a" | "b" | "c" | 1 = Math.random() > 0.5 ? "b" : 1;
if (x === 1) {
const y: number = x; // pass
}
(これは Flow でも TypeScript でも通る。Nullbleの記法が異なるので、そこが両者を横断するに当たって辛いところ…)
動的型付けの環境を後付で解釈するには、このような形になるが、UnionTypeを前提にしたキャストは癖があって、既存の型システムの発想とはやや違った発想が必要になる。ランタイムに影響しない、「型を確定させるための型のためのコード」が出現してくる。
ReduxにFlowtypeを適用してみる
https://github.com/mizchi-sandbox/redux-project-skeleton というプロジェクトを作ってどんなコードになるか実験していた。
(元ネタは https://github.com/reactjs/redux/tree/master/examples/todos-flow)
ここで特記すべきは次の箇所だ。
import type { Connector } from "react-redux";
// ... 中略
const connector: Connector<{}, CounterProps> = connect(({ counter }) => counter);
export default connector(Counter);
const connector: Connector<{}, CounterProps> = connect(({ counter }) => counter);
がキモで、ここを定義することでFlowtypeの推論器が働いて前後のコードが型で保護される。
Connectorの型定義はこうだ。
// ... 略
declare type Connector<OP, P> = {
(component: StatelessComponent<P>): ConnectedComponentClass<OP, P, void, void>;
<Def, St>(component: Class<React$Component<Def, P, St>>): ConnectedComponentClass<OP, P, Def, St>;
};
ややこしいが、<OP, P>
のジェネリクスによって、MountされるComponentの外側からのpropsの型、connectされた内部のpropsの型が確定する。
これはまあいいとして、Reduxは結局Middlewareによって振る舞いが変わるので、外部型定義を自分で手を加えて、型を書き換える必要がある。http://qiita.com/akameco/items/5a0b427c1c5ef2967bac の記事が詳しい。自分も結局 https://github.com/mizchi-sandbox/redux-project-skeleton/blob/redux/src/types/index.js#L16 のようなコードを書いた。
常になにかしらの脱出ハッチを用意して、それを自制しつつ使っていくことが必要になる。
最後に: Redux について
僕は元々、Reduxはあまり好きではなかったのだが、その理由は、関数合成的なコードを多用するのに中間状態が型で保護されないのが不満だったのだが、ある程度手作業が必要でありつつも宣言的なキャストを挟むだけでFlowtypeに推論させることができ、これぐらいならReduxを使ってもいいなと思った。
基本的には外部ライブラリにの型を any に握りつぶしてキャストしつつ、 https://github.com/flowtype/flow-typed から必要なものをポートしていくだけで実用的な環境が手に入る。
みんなも自由な型環境に踊らされつつも色々試してみるのがいいと思う。
追記
ブコメ
以前はReduxの非同期あたりをディスられてたようだがそこは許されたんだろうか
Stateとは同期的に更新されるものであり、ドメインとは分離されるものである、つまりは完全に徹頭徹尾、複雑なものから切り離されたViewStateであるという解釈を受け入れることで妥協できました。
Reduxで非同期を混在したかったのは、当時StateにIndexedDbなどの非同期だがごく短い非同期が混ざることを許容していたので、自分の目的にはそぐわない印象を受けていました。が、結局それらは少数派でありストレージパターンでアクション層に切り離すべきというのは、納得できます。納得できるだけで、どちらが適したパターンかはシチュエーション次第だとも思います。Actionでドメインコードを呼ぶか、Reducerでドメインコードを呼ぶかの違いですね。