アノテーション要件
注記: バージョン0.199以降、Flowは局所型推論を推論アルゴリズムとして使用しています。このセクションのルールは、この推論スキームの主要な設計機能を反映しています。
Flowは、式、変数、パラメータなどの直近のコンテキストから型を容易に推論できるプログラムの部分には、型アノテーションを要求しないように努めています。
変数宣言
次の変数定義を例にとってみましょう。
const len = "abc".length;
len
の型を推論するために必要な情報はすべて、初期化子"abc".length
に含まれています。Flowはまず"abc"
が文字列であること、次に文字列のlength
プロパティが数値であることを判定します。
同じロジックは、すべてのconst
のような初期化に適用できます。状況が少し複雑になるのは、例えば次の例のように、変数の初期化が複数のステートメントにまたがる場合です。
1declare const maybeString: ?string;2
3let len;4if (typeof maybeString === "string") {5 len = maybeString.length;6} else {7 len = 0;8}
Flowは依然としてlen
がnumber
であることを判定できますが、そのためには複数の初期化ステートメントを先読みします。様々な初期化パターンが変数の型をどのように決定し、変数宣言のアノテーションが必要になる場合については、変数宣言のセクションを参照してください。
関数パラメータ
変数宣言とは異なり、この種の「先読み」推論は、関数パラメータの型を決定するために使用できません。次の関数を考えてみましょう。
function getLength(x) {
return x.length;
}
length
プロパティにアクセスして返すことができるx
には多くの種類があります。例えば、length
プロパティを持つオブジェクトや文字列などです。プログラムの後半で、getLength
への次の呼び出しがあったとします。
getLength("abc");
getLength({length: 1});
1つの可能性のある推論は、x
がstring | { length: number }
であるということです。しかし、これはgetLength
の型が現在のプログラムのあらゆる部分によって決定されることを意味します。この種のグローバルな推論は、予期せぬ遠隔作用を引き起こす可能性があるため、回避されます。代わりに、Flowは関数パラメータにアノテーションを付けることを要求します。そのような型アノテーションを提供しないと、パラメータx
に[missing-local-annot]
エラーが表示され、関数の本体はx: any
でチェックされます。
1function getLength(x) { 2 return x.length;3}4
5const n = getLength(1); // no error since getLength's parameter type is 'any'
1:20-1:20: Missing an annotation on `x`. [missing-local-annot]
このエラーを修正するには、x
に次のようにアノテーションを付けるだけです。
1function getLength(x: string) {2 return x.length;3}
同じ要件はクラスメソッドにも適用されます。
1class WrappedString {2 data: string;3 setStringNoAnnotation(x) { 4 this.data = x;5 }6 setString(x: string) {7 this.data = x;8 }9}
3:25-3:25: Missing an annotation on `x`. [missing-local-annot]
コンテキスト型
関数パラメータは、常に明示的にアノテーションする必要はありません。関数呼び出しへのコールバック関数の場合、パラメータ型は直近のコンテキストから容易に推論できます。例えば、次のコードを考えてみましょう。
const arr = [0, 1, 2];
const arrPlusOne = arr.find(x => x % 2 === 1);
Flowはarr
の型をArray<number>
と推論します。これをArray.find
の組み込み情報と組み合わせることで、Flowはx => x % 2 + 1
の型をnumber => mixed
と決定できます。この型はFlowのヒントとして機能し、x
の型をnumber
と決定するのに十分な情報を提供します。
付随するアノテーションは、関数パラメータへのヒントとして機能する可能性があります。例えば
1const fn1: (x: number) => number = x => x + 1;
ただし、アノテーションを関数パラメータのヒントとして使用できない場合もあります。
1const fn2: mixed = x => x + 1;
1:20-1:20: An annotation on `x` is required because Flow cannot infer its type from local context. [missing-local-annot]
この例では、mixed
型にはx
の候補型を抽出するのに十分な情報が含まれていません。
Flowは、オブジェクトのような他の式の中にネストされている場合でも、アノテーションされていないパラメータの型を推論できます。例えば
1const fn3: {f: (number) => void} = {f: (x) => {x as string}};
1:48-1:48: Cannot cast `x` to string because number [1] is incompatible with string [2]. [incompatible-cast]
Flowはx
の型としてnumber
を推論するため、キャストは失敗します。
関数の戻り値の型
関数パラメータとは異なり、関数の戻り値の型は一般的にアノテーションする必要はありません。そのため、上記のgetLength
の定義はFlowエラーを発生させません。
ただし、このルールにはいくつかの注目すべき例外があります。最初の例外はクラスメソッドです。内部data
プロパティを返すgetString
メソッドをWrappedString
クラスに追加した場合
1class WrappedString {2 data: string;3 getString(x: string) { 4 return this.data;5 }6}
3:23-3:22: Missing an annotation on return. [missing-local-annot]
Flowは、getString
の戻り値にアノテーションが不足していることを報告します。
2番目の例外は再帰的な定義です。その簡単な例を以下に示します。
1function foo() { 2 return bar();3}4
5function bar() {6 return foo();7}
1:1-1:14: The following definitions recursively depend on each other, and Flow cannot compute their types: - function [1] depends on other definition [2] - function [3] depends on other definition [4] Please add type annotations to these definitions [5] [6] [definition-cycle]
上記のコードは[definition-cycle]
エラーを発生させます。これは、依存関係サイクルを形成する2つの場所、つまり2つの欠落している戻り値のアノテーションを示しています。いずれかの関数に戻り値のアノテーションを追加することで、問題は解決します。
事実上、メソッドの戻り値に対するアノテーションの要件は、再帰的定義の制限の特殊ケースです。再帰はthis
へのアクセスによって可能です。
ジェネリック呼び出し
ジェネリック関数への呼び出しでは、結果の型は引数として渡された値の型に依存する可能性があります。このセクションでは、型引数を明示的に提供しない場合に、この結果がどのように計算されるかを説明します。
例えば、次の定義を考えてみましょう。
declare function map<T, U>(
f: (T) => U,
array: $ReadOnlyArray<T>,
): Array<U>;
そして、引数x => x + 1
と[1, 2, 3]
を持つ可能性のある呼び出し
map(x => x + 1, [1, 2, 3]);
ここでFlowは、x
の型がnumber
であると推論します。
ジェネリック呼び出しの他の一般的な例としては、ジェネリックSet
クラスのコンストラクタを呼び出すこと、またはReactライブラリのuseState
を呼び出すことが挙げられます。
1const set = new Set([1, 2, 3]);2
3import {useState} from 'react';4const [num, setNum] = useState(42);
4:23-4:34: Cannot call hook [1] because React hooks can only be called within components or hooks. [react-rule-hook]
ここでFlowは、set
の型がSet<number>
であり、num
とsetNum
がそれぞれnumber
と(number) => void
であると推論します。
ソリューションの計算
ジェネリック呼び出しの結果を計算するには、
- ジェネリックな部分を含まない
T
とU
のソリューションを考案し、 map
のシグネチャでソリューションでT
とU
を置き換え、- この新しい
map
のシグネチャを呼び出します。
このプロセスは、2つの目標を念頭に置いて設計されています。
- 健全性。結果は、ステップ(3)に到達したときに正しい呼び出しにつながる必要があります。
- 完全性。Flowが生成する型は、プログラムの他の部分が正常にチェックされるように、できるだけ正確で情報量が多い必要があります。
これらの2つの目標が、上記のmap
の例でどのように機能するかを見てみましょう。
Flowは、$ReadOnlyArray<T>
が[1, 2, 3]
の型と互換性がある必要があることを検出します。したがって、T
がnumber
であると推論できます。
T
を知っていれば、x => x + 1
を正常にチェックできます。パラメータx
はコンテキスト型としてnumber
であるため、結果x + 1
も数値です。この最終的な制約により、U
もnumber
として計算できます。
ジェネリック部分を上記のソリューションで置き換えた後のmap
の新しいシグネチャは次のとおりです。
(f: (number) => number, array: $ReadOnlyArray<number>) => Array<number>
呼び出しが正常にチェックされることは明らかです。
多相呼び出し中のエラー
上記のプロセスがスムーズに進めば、呼び出しに関連するエラーは表示されません。しかし、このプロセスが失敗した場合どうなるでしょうか?
このプロセスが失敗する理由は2つあります。
制約不足の型パラメータ
Flowが型パラメータの型を決定するのに十分な情報がない場合があります。組み込みジェネリックSet
クラスコンストラクタへの呼び出しをもう一度、今回は引数を渡さずに調べてみましょう。
1const set = new Set(); 2set.add("abc");
1:17-1:19: Cannot call `Set` because `T` [1] is underconstrained by new `Set` [2]. Either add explicit type arguments or cast the expression to your expected type. [underconstrained-implicit-instantiation]
new Set
への呼び出し中に、後続のset.add
呼び出しが明らかにT
が文字列になることを示しているにもかかわらず、FlowがT
の型を決定するのに十分な情報を提供していません。型引数の推論は呼び出しに局所的であるため、Flowはこれを決定するために後続のステートメントを先読みしようとしないことを覚えておいてください。
情報がない場合、Flowは任意の型をT
として推論できます。any
、mixed
、empty
などです。この種の決定は、予期せぬ結果につながる可能性があるため、望ましくありません。例えば、Set<empty>
を黙って決定した場合、set.add("abc")
への呼び出しは、string
とempty
の間の非互換性のために失敗し、empty
の由来が明確に示されません。
そのため、このような状況では、[underconstrained-implicit-instantiation]
エラーが発生します。このエラーを修正するには、型注釈を追加します。これにはいくつかの方法があります。
次の2つの方法のいずれかで、呼び出し元に注釈を追加します。
- 明示的な型引数
const set = new Set<string>();
- 初期化変数への注釈
const set: Set<string> = new Set();
- 明示的な型引数
クラスの定義で型パラメーター
T
にデフォルト型を追加します。declare class SetWithDefault<T = string> extends $ReadOnlySet<T> {
constructor(iterable?: ?Iterable<T>): void;
// more methods ...
}呼び出し元に型情報がない場合、Flowは
T
のデフォルト型を推論された型引数として使用します。const defaultSet = new SetWithDefault(); // defaultSet is SetWithDefault<string>
互換性エラー
Flowがジェネリック呼び出しの型パラメーターに対して非ジェネリック型を推論できたとしても、これらの型は、現在の呼び出しまたは後のコードで互換性の問題を引き起こす可能性があります。
たとえば、map
への次の呼び出しがあったとします。
1declare function map<T, U>(f: (T) => U, array: $ReadOnlyArray<T>): Array<U>;2map(x => x + 1, [{}]);
2:10-2:14: Cannot use operator `+` with operands object literal [1] and number [2] [unsafe-addition]
FlowはT
を{}
として推論し、そのためx
の型を{}
とします。これは、アロー関数を確認する際にエラーを引き起こします。なぜなら、オブジェクトでは+
演算子は許可されていないからです。
最後に、一般的なエラーの原因として、ジェネリック呼び出しにおける推論された型が呼び出し自体では正しいものの、コードの後の使用方法を表していないケースがあります。たとえば、次のことを考えてみてください。
1import {useState} from 'react';2const [str, setStr] = useState(""); 3
4declare const maybeString: ?string;5setStr(maybeString);
2:23-2:34: Cannot call hook [1] because React hooks can only be called within components or hooks. [react-rule-hook]5:8-5:18: Cannot call `setStr` with `maybeString` bound to the first parameter because: [incompatible-call] Either null or undefined [1] is incompatible with function type [2]. Or null or undefined [1] is incompatible with string [3].
useState
への呼び出しに文字列""
を渡すと、Flowは状態の型としてstring
を推論します。そのため、setStr
は後で呼び出された場合も入力としてstring
を期待し、したがって?string
を渡すとエラーになります。
繰り返しますが、このエラーを修正するには、useState
を呼び出す際に、期待されるより広い状態の型に注釈を付けるだけで十分です。
const [str, setStr] = useState<?string>("");
空の配列リテラル
空の配列リテラル([]
)は、Flowで特別に扱われます。その動作と要件については、こちらをご覧ください。