註解需求
注意:從 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;
}
有許多種類的 x 可以用來存取並傳回 length 屬性:具有 length 屬性的物件,或字串,僅舉幾例。如果稍後在程式中對 getLength 有以下呼叫
getLength("abc");
getLength({length: 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 錯誤。
不過,這個規則有幾個值得注意的例外。第一個是類別方法。如果我們在 WrappedString 類別中加入一個 getString 方法,用來回傳內部的 data 屬性
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 的回傳值缺少註解。
第二個例外是遞迴定義。一個簡單的範例如下
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] 錯誤,指出形成依賴循環的兩個位置,也就是兩個缺少回傳值註解的位置。為任一函式加入回傳值註解就能解決這個問題。
實際上,方法回傳值需要註解的要求是遞迴定義限制的特殊情況。遞迴是透過存取 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這個新簽章的呼叫。
此程序的設計考量兩個目標
- 健全性。在我們執行步驟 (3) 時,結果需要導致正確的呼叫。
- 完整性。Flow 產生的類型需要盡可能精確且提供資訊,以確保程式其他部分能順利通過檢查。
讓我們看看這兩個目標如何在上述 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>
很容易看出這個呼叫會順利通過檢查。
多型呼叫期間的錯誤
如果上述流程順利進行,您不應該會看到與呼叫相關的任何錯誤。但如果這個流程失敗會發生什麼事?
這個流程可能失敗的原因有兩個
約束不足的型別參數
在某些情況下,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 時,我們沒有提供足夠的資訊讓 Flow 決定 T 的型別,即使後續呼叫 set.add 明確暗示 T 會是字串。請記住,型別參數的推論是針對呼叫進行的,因此 Flow 不會嘗試預測後續陳述式來決定這一點。
在沒有資訊的情況下,Flow 可以自由地將 任何 型別推論為 T:any、mixed、empty 等。這種決定是不理想的,因為它可能導致令人驚訝的結果。例如,如果我們默認決定 Set<empty>,則呼叫 set.add("abc") 會因 string 和 empty 不相容而失敗,而且沒有明確指出 empty 從何而來。
因此,在這種情況下,您會收到 [underconstrained-implicit-instantiation] 錯誤。修正此錯誤的方法是新增型別註解。有幾種可能的方法可以做到這一點
在呼叫位置以兩種方式之一新增註解
- 明確的型別參數
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 以特殊方式處理空陣列文字 ([])。您可以閱讀其 行為和需求。