Flow 0.5.0以降、型パラメータに境界を持つ多相関数とクラスを定義できるようになりました。これは、型パラメータに制約が必要な関数やクラスを記述する際に非常に役立ちます。Flowの境界付きポリモーフィズムの構文は次のようになります。
class BagOfBones<T: Bone> { ... }
function eat<T: Food>(meal: T): Indigestion<T> { ... }
問題
Flowで多相関数を定義する次のコードを考えてみましょう。
function fooBad<T>(obj: T): T {
console.log(Math.abs(obj.x));
return obj;
}
このコードは型チェックを通過しません(そして、通過すべきではありません!)。Math.abs()
によって課せられた追加の要件を考えると、すべての値obj: T
がプロパティx
を持っているわけではなく、ましてやnumber
型のプロパティx
を持っているわけでもありません。
しかし、T
がすべての型ではなく、number
型のx
プロパティを持つオブジェクトの型のみを対象とするようにしたい場合はどうでしょうか?直感的には、その条件が与えられれば、本体は型チェックを通過するはずです。残念ながら、Flow 0.5.0より前は、この条件を強制する唯一の方法は、ポリモーフィズムを完全に諦めることでした!たとえば、次のように記述できます。
// Old lame workaround
function fooStillBad(obj: { x: number }): {x: number } {
console.log(Math.abs(obj.x));
return obj;
}
しかし、この変更は本体の型チェックを通過させる一方で、呼び出しサイト間で情報が失われることになります。たとえば、
// The return type of fooStillBad() is {x: number}
// so Flow thinks result has the type {x: number}
var result = fooStillBad({x: 42, y: "oops"});
// This will be an error since result's type
// doesn't have a property "y"
var test: {x: number; y: string} = result;
解決策
バージョン0.5.0以降、このような型付けの問題は、境界付きポリモーフィズムを使用してエレガントに解決できます。T
などの型パラメータは、型パラメータの範囲を制約する境界を指定できます。たとえば、次のように記述できます。
function fooGood<T: { x: number }>(obj: T): T {
console.log(Math.abs(obj.x));
return obj;
}
これで、T
が{ x: number }
のサブタイプであるという仮定の下で、本体の型チェックが実行されます。さらに、呼び出しサイト間で情報が失われることはありません。上記の例を使用すると、
// With bounded polymorphism, Flow knows the return
// type is {x: number; y: string}
var result = fooGood({x: 42, y: "yay"});
// This works!
var test: {x: number; y: string} = result;
もちろん、多相クラスも境界を指定できます。たとえば、次のコードは型チェックを通過します。
class Store<T: { x: number }> {
obj: T;
constructor(obj: T) { this.obj = obj; }
foo() { console.log(Math.abs(this.obj.x)); }
}
クラスのインスタンス化は適切に制約されます。次のように記述すると、
var store = new Store({x: 42, y: "hi"});
store.obj
の型は{x: number; y: string}
になります。
任意の型を型パラメータの境界として使用できます。型は(上記の例のように)オブジェクト型である必要はありません。スコープ内にある別の型パラメータにすることもできます。たとえば、上記のStore
クラスに次のメソッドを追加することを考えてみましょう。
class Store<T: { x: number }> {
...
bar<U: T>(obj: U): U {
this.obj = obj;
console.log(Math.abs(obj.x));
return obj;
}
}
U
はT
のサブタイプであるため、メソッド本体の型チェックは実行されます(予想どおり、U
はサブタイプの推移性により、T
の境界も満たす必要があります)。これで、次のコードは型チェックを通過します。
// store is a Store<{x: number; y: string}>
var store = new Store({x: 42, y: "yay"});
var result = store.bar({x: 0, y: "hello", z: "world"});
// This works!
var test: {x: number; y: string; z: string } = result;
また、複数の型パラメータを持つ多相定義では、任意の型パラメータが後続の型パラメータの境界に現れる場合があります。これは、次のような例を型チェックする際に役立ちます。
function copyArray<T, S: T>(from: Array<S>, to: Array<T>) {
from.forEach(elem => to.push(elem));
}
なぜこれを作成したのか
境界付きポリモーフィズムを追加することで、Flowの型システムの表現力が大幅に向上します。ジェネリクスの利点を犠牲にすることなく、シグネチャと定義で型パラメータ間の関係を指定できるようになるためです。表現力の向上は、特にライブラリ作成者にとって有用であり、Reactによって提供されるようなフレームワークAPIの宣言を改善するのにも役立つと期待しています。
変換
型注釈やその他のFlow機能と同様に、多相関数とクラス定義は、コードを実行する前に変換する必要があります。変換は、最近リリースされたreact-tools 0.13.0
で利用可能です。