Flow 的下一個版本 0.34 將包含一些物件類型的重大變更
- 屬性變異,
- 預設不變的字典類型,
- 預設協變的方法類型,
- 以及更靈活的 getter 和 setter。
什麼是變異?
定義類型之間的子類型關係是 Flow 作為類型系統的核心責任。這些關係會直接針對簡單類型而決定,或針對複雜類型而根據其組成部分定義。
變異描述複雜類型的子類型關係,因為它與其組成部分的子類型關係有關。
例如,Flow 直接編碼了 string 是 ?string 子類型的知識。直覺上,string 類型包含字串值,而 ?string 類型包含 null、undefined,以及字串值,因此,前者的成員資格自然暗示後者的成員資格。
兩個函式類型之間的子類型關係並非如此直接。相反地,它是從函式的參數和回傳類型之間的子類型關係衍生而來的。
讓我們看看這對兩個簡單函式類型如何運作
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。
輸入和輸出
要記住某個東西是協變還是反變的便捷方式,就是思考「輸入」和「輸出」。
參數位於輸入位置,通常稱為「負面」位置。複雜類型在其輸入位置中為反變。
傳回值是輸出位置,通常稱為「正面」位置。複雜類型在其輸出位置中為協變。
屬性不變性
函式類型由參數和傳回值類型組成,物件類型也是由屬性類型組成。因此,物件之間的子類型關係來自其屬性的子類型關係。
然而,與具有輸入參數和輸出傳回值的函式不同,物件屬性可以讀取和寫入。也就是說,屬性同時是輸入和輸出。
讓我們看看這如何套用在兩個簡單的物件類型上
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 的主體將 null 寫入 o.p,則 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 可以用 null 覆寫屬性 p。在 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 提供了一種簡寫方式來寫入函式的物件屬性。
var o = {
m(x) {
return x * 2
}
}
Flow 現在將使用這種簡寫方法語法的屬性預設解釋為協變。這表示寫入屬性 m 會產生錯誤。
如果您不想要協變,可以使用長格式語法
var o = {
m: function(x) {
return x * 2;
}
}
更靈活的取得器和設定器
在 Flow 0.33 及更早版本中,取得器和設定器必須在各自的回傳類型和參數類型上完全一致。Flow 0.34 解除了這個限制。
這表示您可以撰寫像以下這樣的程式碼
// @flow
declare var x: string;
var o = {
get x(): string {
return x;
},
set x(value: ?string) {
x = value || "default";
}
}