Flowの次のリリース、0.34では、オブジェクト型にいくつかの重要な変更が含まれます。
- プロパティの変位性、
- デフォルトで不変な辞書型、
- デフォルトで共変なメソッド型、
- そして、より柔軟なgetterとsetterです。
変位性とは?
型システムとして、Flowの核となる役割は、型間のサブタイプ関係を定義することです。これらの関係は、単純な型の場合は直接決定され、複雑な型の場合は、その構成要素によって定義されます。
変位性とは、複雑な型のサブタイプ関係を、その構成要素のサブタイプ関係に関連付けて記述したものです。
例えば、Flowは、string
が?string
のサブタイプであるという知識を直接エンコードしています。直感的に、string
型は文字列値を含み、?string
型はnull
、undefined
、そして文字列値も含むため、前者に属することは自然に後者に属することを意味します。
2つの関数型間のサブタイプ関係は、それほど直接的ではありません。むしろ、関数の引数と戻り値の型のサブタイプ関係から導き出されます。
2つの単純な関数型でこれがどのように機能するかを見てみましょう。
type F1 = (x: P1) => R1;
type F2 = (x: P2) => R2;
F2
がF1
のサブタイプであるかどうかは、P1
とP2
、およびR1
とR2
の関係によって異なります。B <: A
という表記を使用して、B
はA
のサブタイプであることを表しましょう。
P1 <: P2
かつ R2 <: R1
の場合、F2 <: F1
となります。パラメータの関係が逆になっていることに注意してください。専門用語では、関数型は引数型に対して「反変」であり、戻り値の型に対して「共変」であると言えます。
例を見てみましょう。
function f(callback: (x: string) => ?number): number {
return callback("hi") || 0;
}
どのような種類の関数をf
に渡すことができるでしょうか?上記のサブタイプ規則に基づくと、引数型がstring
のスーパータイプであり、戻り値の型が?number
のサブタイプである関数を渡すことができます。
function g(x: ?string): number {
return x ? x.length : 0;
}
f(g);
f
の本体は、string
値のみをg
に渡します。これは、g
が?string
を受け取ることで少なくともstring
を受け取るため、安全です。逆に、g
はnumber
値のみをf
に返します。これは、f
が?number
を処理することで少なくともnumber
を処理するため、安全です。
入力と出力
何かが共変か反変かを覚えるための便利な方法は、「入力」と「出力」について考えることです。
パラメータは*入力*の位置にあり、しばしば「負」の位置と呼ばれます。複雑な型は、入力位置で反変です。
戻り値は*出力*の位置にあり、しばしば「正」の位置と呼ばれます。複雑な型は、出力位置で共変です。
プロパティの不変性
関数型が引数型と戻り値型で構成されているのと同様に、オブジェクト型もプロパティ型で構成されています。したがって、オブジェクト間のサブタイプ関係は、それらのプロパティのサブタイプ関係から導き出されます。
ただし、入力パラメータと出力戻り値を持つ関数とは異なり、オブジェクトのプロパティは読み書きできます。つまり、プロパティは*入力と出力の両方*です。
2つの単純なオブジェクト型でこれがどのように機能するかを見てみましょう。
type O1 = {p: T1};
type O2 = {p: T2};
関数型と同様に、O2
がO1
のサブタイプであるかどうかは、その構成要素であるT1
とT2
の関係によって異なります。
ここでは、T2 <: T1
*かつ* T1 <: T2
の場合、O2 <: O1
となります。専門用語では、オブジェクト型はプロパティ型に対して「不変」です。
例を見てみましょう。
function f(o: {p: ?string}): void {
// We can read p from o
let len: number;
if (o.p) {
len = o.p.length;
} else {
len = 0;
}
// We can also write into p
o.p = null;
}
それでは、どのような種類のオブジェクトをf
に渡すことができるでしょうか? サブタイプのプロパティを持つオブジェクトを渡そうとすると、エラーが発生します。
var o1: {p: string} = {p: ""};
f(o1);
function f(o: {p: ?string}) {}
^ null. This type is incompatible with
var o1: {p: string} = {p: ""};
^ string
function f(o: {p: ?string}) {}
^ undefined. This type is incompatible with
var o1: {p: string} = {p: ""};
^ string
Flowはここでエラーを正しく特定しました。 f
の本体がo.p
にnull
を書き込むと、o1.p
はもはや型string
ではなくなります。
スーパータイプのプロパティを持つオブジェクトを渡そうとすると、再びエラーが発生します。
var o2: {p: ?(string|number)} = {p: 0};
f(o2);
var o1: {p: ?(string|number)} = {p: ""};
^ number. This type is incompatible with
function f(o: {p: ?string}) {}
^ string
ここでも、Flowはエラーを正しく特定します。なぜなら、f
がo
からp
を読み取ろうとすると、数値が見つかるからです。
プロパティの変位性
プロパティは読み書きできるため、オブジェクトはプロパティ型に対して不変でなければなりません。しかし、読み書き*できる*からといって、常に読み書きするとは限りません。
null許容文字列プロパティの長さを取得する関数を考えてみましょう。
function f(o: {p: ?string}): number {
return o.p ? o.p.length : 0;
}
o.p
に書き込むことはないので、プロパティp
の型が?string
のサブタイプであるオブジェクトを渡すことができるはずです。 これまでは、Flowではこれは不可能でした。
プロパティの変位性を使用すると、オブジェクトのプロパティを共変および反変として明示的にアノテーションできます。たとえば、上記の関数を書き直すことができます。
function f(o: {+p: ?string}): number {
return o.p ? o.p.length : 0;
}
var o: {p: string} = {p: ""};
f(o); // no type error!
共変なプロパティが出力位置にのみ現れることが重要です。共変なプロパティに書き込むとエラーになります。
function f(o: {+p: ?string}) {
o.p = null;
}
o.p = null;
^ object type. Covariant property `p` incompatible with contravariant use in
o.p = null;
^ assignment of property `p`
逆に、関数がプロパティに書き込むだけの場合は、プロパティを反変としてアノテーションできます。これは、たとえば、オブジェクトをデフォルト値で初期化する関数で発生する可能性があります。
function g(o: {-p: string}): void {
o.p = "default";
}
var o: {p: ?string} = {p: null};
g(o);
反変なプロパティは入力位置にのみ現れることができます。反変なプロパティから読み取るとエラーになります。
function f(o: {-p: string}) {
o.p.length;
}
o.p.length;
^ object type. Contravariant property `p` incompatible with covariant use in
o.p.length;
^ property `p`
デフォルトで不変な辞書型
オブジェクト型 {[key: string]: ?number}
は、マップとして使用できるオブジェクトを記述します。任意のプロパティを読み取ることができ、Flowは結果の型を?number
として推論します。また、任意のプロパティにnull
、undefined
、またはnumber
を書き込むこともできます。
Flow 0.33以前では、これらの辞書型は型システムによって共変として扱われていました。たとえば、Flowは次のコードを受け入れました。
function f(o: {[key: string]: ?number}) {
o.p = null;
}
declare var o: {p: number};
f(o);
これは、f
がプロパティp
をnull
で上書きできるため、安全ではありません。Flow 0.34では、辞書は名前付きプロパティと同様に不変です。同じコードで、次の型エラーが発生するようになりました。
function f(o: {[key: string]: ?number}) {}
^ null. This type is incompatible with
declare var o: {p: number};
^ number
function f(o: {[key: string]: ?number}) {}
^ undefined. This type is incompatible with
declare var o: {p: number};
^ number
ただし、共変および反変の辞書は非常に役立つ場合があります。これをサポートするために、名前付きプロパティの変位性をサポートするために使用されるのと同じ構文を辞書にも使用できます。
function f(o: {+[key: string]: ?number}) {}
declare var o: {p: number};
f(o); // no type error!
デフォルトで共変なメソッド型
ES6では、関数であるオブジェクトプロパティを記述するための shorthand な方法が導入されました。
var o = {
m(x) {
return x * 2
}
}
Flowは、このshorthandメソッド構文を使用するプロパティをデフォルトで共変として解釈するようになりました。つまり、プロパティm
に書き込むとエラーになります。
共変にしたくない場合は、long form 構文を使用できます。
var o = {
m: function(x) {
return x * 2;
}
}
より柔軟なGetterとSetter
Flow 0.33以前では、getterとsetterはそれぞれ戻り値の型とパラメータの型が完全に一致している必要がありました。Flow 0.34では、この制限が解除されました。
これは、次のようなコードを記述できることを意味します。
// @flow
declare var x: string;
var o = {
get x(): string {
return x;
},
set x(value: ?string) {
x = value || "default";
}
}