メインコンテンツへスキップ

アノテーション要件

注記: バージョン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は依然としてlennumberであることを判定できますが、そのためには複数の初期化ステートメントを先読みします。様々な初期化パターンが変数の型をどのように決定し、変数宣言のアノテーションが必要になる場合については、変数宣言のセクションを参照してください。

関数パラメータ

変数宣言とは異なり、この種の「先読み」推論は、関数パラメータの型を決定するために使用できません。次の関数を考えてみましょう。

function getLength(x) {
return x.length;
}

lengthプロパティにアクセスして返すことができるxには多くの種類があります。例えば、lengthプロパティを持つオブジェクトや文字列などです。プログラムの後半で、getLengthへの次の呼び出しがあったとします。

getLength("abc");
getLength({length: 1});

1つの可能性のある推論は、xstring | { 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>であり、numsetNumがそれぞれnumber(number) => voidであると推論します。

ソリューションの計算

ジェネリック呼び出しの結果を計算するには、

  1. ジェネリックな部分を含まないTUのソリューションを考案し、
  2. mapのシグネチャでソリューションでTUを置き換え、
  3. この新しいmapのシグネチャを呼び出します。

このプロセスは、2つの目標を念頭に置いて設計されています。

  • 健全性。結果は、ステップ(3)に到達したときに正しい呼び出しにつながる必要があります。
  • 完全性。Flowが生成する型は、プログラムの他の部分が正常にチェックされるように、できるだけ正確で情報量が多い必要があります。

これらの2つの目標が、上記のmapの例でどのように機能するかを見てみましょう。

Flowは、$ReadOnlyArray<T>[1, 2, 3]の型と互換性がある必要があることを検出します。したがって、Tnumberであると推論できます。

Tを知っていれば、x => x + 1を正常にチェックできます。パラメータxはコンテキスト型としてnumberであるため、結果x + 1も数値です。この最終的な制約により、Unumberとして計算できます。

ジェネリック部分を上記のソリューションで置き換えた後の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として推論できます。anymixedemptyなどです。この種の決定は、予期せぬ結果につながる可能性があるため、望ましくありません。例えば、Set<empty>を黙って決定した場合、set.add("abc")への呼び出しは、stringemptyの間の非互換性のために失敗し、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で特別に扱われます。その動作と要件については、こちらをご覧ください。