Flow靜態資料類型的檢查工具,10分鐘快速入門


Flow是一種靜態資料類型的輔助檢查工具,Flow的功能是讓現有的JavaScript語法可以作預先資料類型的定義,在開發過程中進行自動檢查,當然在最後編譯時,可以用工具來移除這些標記。在這之前,你可能有聽過TypeScript程式語言,它其中也有靜態資料類型的特性,常常會拿來與Flow作比較。不過,相較於TypeScript是另外制作一套超集(superset)程式語言,最後再經過編譯為JavaScript程式碼來執行。Flow走的則是非強制性與非侵入式的路線,提供了另一種在現有JavaScript應用上的選擇。總結來說,這兩種方式的目的是相似的,各自有優點也有不足之處,不過青菜蘿蔔各有所好,要選擇哪一種方式就看你的選擇。

當然,並沒有要求什麼時候一定要用這類的工具,只是這種作法可以讓你的程式碼更具強健性與閱讀性,也可以直接避去很多不必要的資料類型使用上的問題,這種開發方式目前在許多函式庫專案,或是以JavaScript應用為主的公司中已經都是必用工具。

其實,會有這個議題的出現是,主因是JavaScript是一種動態資料類型的程式語言,動態資料類型代表在程式碼中,變數或常數會自動依照指定值變更資料類型,這是直譯式腳本語言的常見特性,是優點也是很大的缺點。常常出現很嚴重的問題,例如開發者經常會因為指定值的類型錯誤,造成不可預期的結果,有些函式庫如果沒有仔細觀看文件或是文件寫得不清不楚,也容易造成誤用的情況。加入靜態資料類型後,代表可以讓變數或常數在宣告(定義)時,就可以指定必需使用哪一種資料類型,之後在使用時如果發生資料類型不相符時,就會發出預先的警告,這對於開發JavaScript應用程式相當有幫助。

這種資料類型不符的情況在JavaScript程式中非常容易發生,例如以下的例子:

function foo(x) {
  return x + 10;
}
foo('Hello!');

x這個傳入參數,我們在函式定義時原本希望它是個數字類型,但最後使用呼叫函式時則用了字串類型。你知道最後的結果會是什麼嗎?"Hello!10",這是因為加號(+)在JavaScript語言中,除了作為數字的加運算外,也可以當作字串的串接運算。想當然爾,這並不是我們想要的結果。由這個小例子你可以想見,如果在很多人一起開發某個大的JavaScript應用時,這種資料類型的輸出輸入問題,會需要花多少時間進行溝通與明確的文件化,不然昨天剛上班的程式設計師怎麼會知道哪個函式的傳入參數是要什麼類型?或是回傳值是要什麼才可以?文件化也只能提供說明,無法檢查各種情況。

Flow工具十分簡單就可以使用,加上一個程式碼註解作為// @flow的指示標記在程式碼檔案的最上面,這樣Flow工具就知道這個檔案是需要進行資料類型檢查的,然後在x傳入參數的後面加上類型註釋(Type Annotations),像x: number這樣,代表這個x的類型會被定義為數字類型。程式碼如下所示:

// @flow
function foo(x: number) {
  return x + 10;
}
foo('Hello!');

經過flow工具的檢查後,會出現類似像下面的警告訊息,告訴你在哪一行的程式碼出現了資料類型不符合的情況:

5: foo('Hello!');
   ^^^^^^^^^^^^^ function call
5: foo('Hello!');
       ^^^^^^^^ string. This type is incompatible with
2: function foo(x: number) {
                   ^^^^^^ number

在實務上使用時,因為現在已經有很多搭配程式碼編輯程式的外掛,在你撰寫程式碼時就會立即檢查,回報目前有問題的程式碼。以下列出常用的幾個開發工具與對應外掛:

註: 搭配程式碼編輯程式的外掛只是輔助,你還是要事先在電腦中安裝Flow工具的執行程式

最後,在上面的範例中,你也可以對函式的回傳值作靜態資料類型的註釋,像下面這樣的範例:

// @flow
function foo(x: number): number {
  return x + 10;
}
foo('Hello!');

註: Flow專案由Facebook公司維護與發佈。TypeScript專案由Microsoft公司維護與發佈。


10分鐘分隔線~~以下是超過10分鐘的內容。


安裝Flow

Flow目前可以支援Mac OSX、Linux(64位元)、Windows(64位元),你可以從以下的四種安裝方式選擇"其中一種":

  • 直接從Flow的發佈頁面下載可執行檔案,加到電腦中的PATH(路徑),讓flow指令可以在命令列視窗存取即可。

  • 透過npm安裝即可,可以安裝在全域(global)或是各別專案中。下面為安裝在專案中的指令:
npm install --save-dev flow-bin
  • Mac OS X中可以使用homebrew安裝:
brew update
brew install flow
  • 透過OCaml OPAM套裝管理程式打包與安裝,請見Flow的Github頁面

Flow簡單使用三步驟

  • 第1步: 初始化專案。在你的專案根目錄的用命令列工具輸入,這將會建立一個.flowconfig檔案,如果檔案已經存在就不需要再進行初始化,這個設定檔一樣是可以加入自訂的設定值,請參考Advanced Configuration這裡的說明,有很多專案裡面都已經內附這個設定檔,尤其Facebook的專案:
flow init
  • 第2步: 在程式碼檔案中加入要作類型檢查的註解。一般都在程式碼檔案的最上面一行加入:
// @flow

/* @flow */
  • 第3步: 進行檢查。有安裝搭配編輯工具的外掛時,編輯工具會輔助顯示,或是用下面的命令列指令來進行檢查:
flow check

轉換(編譯)有Flow標記的程式碼

在開發的最後階段要將原本有使用Flow標記,或是有類型註釋的程式碼,進行清除或轉換。

轉換的工作要使用babel編譯器,這也是目前較推薦的方式。有些其他轉換工具程式的專案過舊,有可能不支援最近的Flow版本。

使用babel編輯器如果以命令列工具為主,可以使用下面的指令來安裝在全域中:

npm install -g babel-cli

再來加裝額外移除Flow標記的npm套件在你的專案中:

npm install --save-dev babel-plugin-transform-flow-strip-types

然後建立一個.babelrc設定檔案,檔案內容如下:

{
  "plugins": [
    "transform-flow-strip-types"
  ]
}

完成設定後,之後babel在編譯時就會一併轉換Flow標記。下面的指令是把src目錄的檔案編譯到dist目錄中:

babel src -d dist

當然,babel的使用方式不是只有上面說的這種命令列指令,你可以視專案的使用情況來進行設定,上述的說明只是個簡單的使用範例而已。

Flow支援的資料類型

Flow用起來是的確是簡單,但裡面的內容很多,原因是有很多種不同的使用情況需要搭配。不過在開始說明前,我需要再次提醒,Flow畢竟只是個輔助用的檢查工具,它並沒有強制性或侵入性的能力,也就是說用了它並不會強迫去改動你的程式碼,或是在程式碼中額外加入什麼,就算你不理會也是可以照樣執行程式(不過要記得先移除那些類型註釋,不然會有錯誤)。而且,Flow更加不可能自動修正程式臭蟲或不正確的語法。這些檢查的建議訊息只是個提醒而已,程式碼仍然需要靠開發者自行去修正或調整。

基礎資料類型

Flow支援JavaScript的基礎資料類型,如下面的列表:

  • boolean
  • number
  • string
  • null
  • void

其中最特別的是void類型,它就是JavaScript中的undefined類型。

這裡可能需要注意的是,在JavaScript中undefinednull的值會相等但類型不同,意思是作值相等比較時,像(undefined == null)時會為true,有時候在一些執行期間的檢查時,大部份情況可能會用值相等比較來檢查這兩個類型的值。

所有的類型都可以使用垂直線符號(|)作為聯合使用,例如string | number指的是兩種類型都可以使用,這是一種聯合的類型,聯合(Union)類型最下面有再說明。

最特別的是可選的(Optional)類型的設計,這與某些程式語言(Swift)中的設計有點類似,可選類型代表這個變數或常數的值有可能不存在,也就是允許它除了是某個類型外,也可以是nullundefined類型。要使用可選類型,就是在類型名稱定義前加上問號(?),例如?string這樣,下面是一個簡單的範例:

var o: ?string = null;

任何的資料類型

因為有一些情況可能不需要定義的過於嚴格,或是還在開發中正在測試與調整,這是一種作為漸進地的改善程式碼的方式,Flow提供了兩種特殊的類型可以作為鬆散的資料類型定義:

  • any: 相當於不檢查。既是所有類型的超集(supertype),也是所有類型的子集(subtype)
  • mixed: 類似於any是所有類型的超集(supertype),但不同於any的是,它不是所有類型的子集(subtype)

mixed是一個特別的類型,中文是混合的意思,mixed算是any的"囉嗦"進化類型。mixed用在函式的輸入(傳入參數)與輸出(回傳值)時,會有不一樣的狀態,例如以下的範例會出現警告:

function foo(x: mixed): string {
  return x + '10';
}
foo('Hello!');
foo(1);

警告訊息如下:

mixed: This type is incompatible with the expected return type of string

這原因是因為雖然輸入時可以用mixed,但Flow會認為函式中不見得最後一定會回傳string類型,所以會要求你要在函式中的程式碼,要加入檢查對傳入類型在執行期間的類型檢查程式碼,例如像下面的範例這樣才能通過檢查,下面是修改過的範例:

function foo(x: mixed): string {
  if(typeof x === 'number' && typeof x === 'string' ){
    return x + '10';
  }else{
    throw new Error("Invalid x type");
  }
}
foo('Hello!');
foo(1);

mixed雖然"囉嗦",但它是用來漸進取代any使用的,有時候往往開發者健忘或偷懶忘了作傳入值在執行期間的類型檢查,結果後面要花更多的時間才能找出錯誤點,這個類型的設計大概是為了提早預防這樣的情況。

複合式的資料類型

陣列(Array)

陣列類型使用的是Array<T>,例如Array<number>,會限定陣列中的值只能使用數字的資料類型。當然你也可以加入埀直線(|)來定義允許多種類型,例如Array<number|string>

物件(Object)

物件類型會比較麻煩,主要原因是在JavaScript中所有的資料類型大概都可以算是物件,就算是基礎資料類型也有對應的包裝物件,再加上有個例外的null類型的typeof回傳值也是物件。

物件類型在Flow中的使用,基本上要分作兩大部份來說明。

第一種是單指Object這個類型,Flow會判斷所有的基礎資料類型都"不是"屬於這個類型的,以下的範例全部都會有警告:

//以下都有flow警告
(0: Object);
("": Object);
(true: Object);
(null: Object);
(undefined: Object);

其他的複合式資料類型,除了陣列之外,都會認為是物件類型。如下面的範例:

({foo: "foo"}: Object);
(function() {}: Object);
(class {}: Object);
([]: Object); // Flow不認為陣列是屬於物件

注意: 上面有兩個特例,typeof nulltypeof []都是回傳'object'。也就是說在JavaScript的標準定義中,null陣列都是屬於物件類型。所以,Flow工具中的檢查會與JavaScript中有差異,這一點要注意。

註: typeof在Flow工具中有另外的用途,詳見Typeof的說明。

第二種方式是要定義出完整的物件的字面文字結構,像{ x1: T1; x2: T2; x3: T3;}的語法,用這個結構來檢查,以下為範例:

let object: {foo: string, bar: number} = {foo: "foo", bar: 0};

object.foo = 111; //flow警告
object.bar = '111'; //flow警告

函式(Function)

上面已經有看到,函式也屬於物件(Object)類型,當然也有自己的Function類型,函式的類型也可以從兩大部份來看。

第一是單指Function這個類型,可以用來定義變數或常數的類型。如下面的程式碼範例:

var anyFunction: Function = () => {};

第二指的是函式中的用法,上面已經有看到函式的輸出(回傳值)與輸入(傳入參數)的用法範例。例如以下的範例:

function foo(x: number): number {
  return x + 10;
}

因為函式有很多種不同的使用情況,實際上可能會複雜很多,Flow工具可以支援目前最新的arrow functions、async functions與generator functions,詳見官方的這篇Functions的說明。

類別(Class)

類別是ES6(ES2015)中新式的特性,類別定義語法目前仍然只是原型的語法糖。類別本身也屬於一種物件(Object)類型。

Class的定義中可以包含對類別成員的類型定義,就如同函式或變數的類型定義一樣,如下面的範例:

class MyClass {
  foo: string;
  constructor(foo: string) {
    this.foo = foo;
  }
  bar(): string {
    return this.foo;
  }
}

var myInstance: MyClass = new MyClass("foo");
(myInstance.foo: string);
(myInstance.bar(): string);

Flow支援以類別作為一種資料類型的定義方式,不過這只能使用於子類別都是以extends繼承自此類別的情況。除了這種情況之外,Flow工具另外提供了介面(Interface)的宣告,只是為了方便類別的類型定義,這個介面定義目前並不是屬於JavaScript語言的特性之一,所以會與其他的類型註釋一樣,在編譯時會自動被移除。例如以下的範例:

interface Comparable<T> {
  compare(a: T, b: T): number;
}

此外,由於類別本身就是原型的語法糖,Flow也支援由原型來定義成員的類型檢查。

另外有一種特別的類型稱為Class<T>,它代表的是以這個類別作為類型時的實例,例如以下的範例:

class MyClass {
  foo(x: string) {};
}

var myClass: Class<MyClass> = MyClass;
var myInstance2 = new myClass("foo");

類別的使用情況也可能會複雜,尤其是涉及多型與實例的情況,詳見Flow網站提供的Classes內容。

類型別名

類型別名(Type Alias)提供了可以預先定義與集中程式碼中所需要的類型,一個簡單的範例如下:

type T = Array<string>;
var x: T = [];
x["Hi"] = 2; //flow警告

類型別名(Type Alias)也可以用於複雜的應用情況,詳見Flow網站提供的Type Aliases內容。

泛用類型(T)

泛用(Generics)類型通常搭配類別使用,用於不確定的類型定義情況,例如以下的範例:

class GenericClass<T> {
  x: T;
  constructor(x: T) {
    this.x = x;
  }
}

var numberInstance: GenericClass<number> = new GenericClass(0);
var stringInstance: GenericClass<string> = new GenericClass("");

其他的資料類型定義

字面文字(literal)類型

字面文字類型指的是以實際的值作為資料類型,可用的值有三種,即數字、字串或布林值,例如以下的範例:

("foo": "foo");
("bar": "foo"); // `"bar"` is not exactly `"foo"`
("fo"+"o": "foo"); // even simple expressions lose literal information

(1: 1);
(2: 1); // `2` is not exactly `1`
(1+1: 2); // even simple expressions lose literal information

(true: true);
(true: false); // `true` is not exactly `false`

// boolean expressions *do* preserve literal information
(!true: false);
(true && false: false);
(true || false: true);

字面文字類型搭配聯合的類型可以作為列舉(enums)來使用,例如以下的一個撲克牌的類型範例:

type Suit =
  | "Diamonds"
  | "Clubs"
  | "Hearts"
  | "Spades";
type Rank =
  | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
  | "Jack"
  | "Queen"
  | "King"
  | "Ace";
type Card = {
  suit: Suit,
  rank: Rank,
}

聯合(Union)與交叉(Intersection)類型

聯合(Union)類型在上面已經看過很多次了,就是把不同的類型組合起來,是一種OR(或)的類型組合,一個簡單的範例如下:

type U = number | string;
var x: U = 1;
x = "two";

交叉(Intersection)類型則是使用與符號(&),是把不同的類型組合起來,不過它需要同時檢查符合於組合中的所有類型,可能用到的情況會更少。

React

Flow很特別的是它有支援React的相關語法,React元件中的PropTypes可以用Flow工具檢查對應的資料類型,此外也可以使用Flow來檢查state(狀態)的相對應結果。詳見Flow網站提供的React內容。

結論

Flow工具提供了豐富的檢查類型內容,對於各種使用的情況都可以提供對應的資料類型檢查機制。雖然它才發佈不久,但受到很大的支持,這個工具對於開發JavaScript程式相當有幫助,你可以不妨試試看。