本文為翻譯文章,原文鏈接見文末
在第一部分中我們了解了許多基礎(chǔ)知識,結(jié)束了語法的學習,我們可以進入下一個更有趣的部分:使用靜態(tài)類型的優(yōu)勢和劣勢
使用靜態(tài)類型的優(yōu)勢
靜態(tài)類型會給我們寫代碼提供很多好處。下面我們就來探討一下。
優(yōu)勢一:你可以盡早發(fā)現(xiàn)bug和錯誤
靜態(tài)類型檢查允許我們在程序沒有運行之前就可以確定我們所設(shè)定的確定性是否是對的。一旦有違反這些既定規(guī)則的行為,它能在運行之前就發(fā)現(xiàn),而不是在運行時。
一個例子:假設(shè)我們有一個簡單的方法,輸入半徑,計算面積:
const calculateArea = (radius) => 3.14 * radius * radius;
var area = calculateArea(3);
// 28.26
現(xiàn)在,如果你要給radius傳一個非數(shù)值的值(例如‘im evil’)
var area = calculateArea('im evil');
// NaN
會返回一個NaN。如果其他某些功能要依賴這個方法,并且需要始終返回一個數(shù)值類型,那在這種返回結(jié)果下,就會導致一個bug甚至崩潰。不理想。。。
當我們使用了靜態(tài)類型,我們就可以準確確認一個方法的輸入與輸出:
const calculateArea = (radius: number): number => 3.14 * radius * radius;
現(xiàn)在在試著給calculateArea方法傳入一個非數(shù)值類型,這時候Flow就會顯示如下信息:
calculateArea('Im evil');
^^^^^^^^^^^^^^^^^^^^^^^^^ function call
calculateArea('Im evil');
^^^^^^^^^ string. This type is incompatible with
const calculateArea = (radius: number): number => 3.14 * radius * radius;
^^^^^^ number
現(xiàn)在我們能確保這個方法只會接受有效的數(shù)值作為參數(shù),并且返回一個有效的數(shù)值。
因為類型檢查器會在你編碼的時候就告訴你錯誤,所以這也就比你把代碼交付到客戶手中才發(fā)現(xiàn)一些錯誤要更方面(或者說付出更少的開發(fā)與維護成本)。
優(yōu)勢二:起到在線文檔的功能
對我們和其他接觸我們代碼的人而言,類型就好像是一個文檔。
通過我之前工作項目里一大段代碼中的一個方法,我們可以看看具體是怎么用的:
function calculatePayoutDate(quote, amount, paymentMethod) {
let payoutDate;
/* business logic */
return payoutDate;
}
第一眼看到這個方法(即使是第二眼、第三眼……),我沒法搞清楚要如何使用它。
quote參數(shù)是一個數(shù)值型么?還是一個boolean型?paymentMethod是一個對象么?還是僅僅就是一個代表著支付方式的字符串?這個方法會返回一個表示日期的字符串么?還是一個Date對象?
沒有任何的提示……
這個時候我就只能各種看業(yè)務邏輯,在代碼庫里到處檢索,直到我最終搞清楚了。但是,就為了要理解這簡單的一個方法,費了九牛二虎之力。
如果是另一種情況,我們向下面這樣來寫:
function calculatePayoutDate(
quote: boolean,
amount: number,
paymentMethod: string): Date {
let payoutDate;
/* business logic */
return payoutDate;
}
這一下就讓方法的輸入?yún)?shù)和返回值的類型變得清晰了。這一場景展示了,我們?nèi)绾斡渺o態(tài)類型來表達方法的含義。通過這樣做,我們可以很好得與其他開發(fā)者交流,什么是我們的方法所期望的,而什么值又是他們希望從方法中獲取的。下一次他們使用到這個方法的時候,就不會產(chǎn)生疑問了。
當然,也有人會說,在方法上加上一段注釋文檔不也能解決同樣的問題么:
/*
@function Determines the payout date for a purchase
@param {boolean} quote - Is this for a price quote?
@param {boolean} amount - Purchase amount
@param {string} paymentMethod - Type of payment method used for this purchase
*/
function calculatePayoutDate(quote, amount, paymentMethod) {
let payoutDate;
/* .... Business logic .... */
return payoutDate;
};
這確實是個辦法。但是這種方式會更冗長。除了冗長之外,由于可依賴性較低,同時缺乏結(jié)構(gòu)化,因此像這樣的代碼注釋會難以維護。一些開發(fā)者會寫出不錯的注釋,而另一些人的注釋可能含糊不清,甚至有些根本忘了寫注釋。
尤其是當你重構(gòu)代碼的時候,你很容易就會忘記去更新相應的注釋。然而,類型聲明有著定義好的語法和結(jié)構(gòu),不會隨著時間推移而消失——它們是被寫在代碼里的。
優(yōu)勢三:減少了復雜的錯誤處理
類型可以幫助我們減少復雜的錯誤處理。讓我們重新回去看一看calculateArea方法,看看它是怎么做到的:
const calculateAreas = (radii) => {
var areas = [];
for (let i = 0; i < radii.length; i++) {
areas[i] = PI * (radii[i] * radii[i]);
}
return areas;
};
這個方法是有效的,但是它不能正確處理無效的輸入?yún)?shù)。如果你想要確保這個方法能夠正確處理輸入?yún)?shù)非有效數(shù)組的情況,你可能需要把它改成下面這個樣子:
const calculateAreas = (radii) => {
// Handle undefined or null input
if (!radii) {
throw new Error("Argument is missing");
}
// Handle non-array inputs
if (!Array.isArray(radii)) {
throw new Error("Argument must be an array");
}
var areas = [];
for (var i = 0; i < radii.length; i++) {
if (typeof radii[i] !== "number") {
throw new Error("Array must contain valid numbers only");
} else {
areas[i] = 3.14 * (radii[i] * radii[i]);
}
}
return areas;
};
哈?就這點功能就要碼這么多行。
但是有了靜態(tài)類型,就變得簡單了:
const calculateAreas = (radii: Array<number>): Array<number> => {
var areas = [];
for (var i = 0; i < radii.length; i++) {
areas[i] = 3.14 * (radii[i] * radii[i]);
}
return areas;
};
現(xiàn)在,這個方法實際上看起來就和最初的很類似,沒有了令人視覺上混亂的錯誤處理。這還是很容易能看出它的優(yōu)勢的,對吧?
優(yōu)勢四:使你在重構(gòu)時更有信心
我會通過一個趣事來解釋這一點:我曾經(jīng)的工作涉及一個非常大型的代碼庫,我們需要更新里面的User類的一個方法。特別的,我們需要將方法中的一個參數(shù)從string變?yōu)?code>object。
我改了這個方法,但是當我提交更新的時候卻感到手腳發(fā)涼——這個方法在代碼庫里有太多的地方調(diào)用了,以至于我不確定我是否已經(jīng)修改了所有的實例。如果有些不在測試幫助文件里的深層調(diào)用被我遺留了怎么辦呢?
唯一可以搞清楚這一點的方法就是,提交這份代碼并且祈禱不會出現(xiàn)一些錯誤。
使用靜態(tài)類型將會避免這種情況。這會保證,如果更新了方法,并且相應地更新了類型定義,那么類型檢查器將會幫我去捕獲我遺漏的錯誤。我所需要做的就是,瀏覽這些錯誤并修復它們。
優(yōu)勢五:將數(shù)據(jù)和行為分離
一個較少談及的關(guān)于靜態(tài)類型的優(yōu)點是,它可以幫助我們進行數(shù)據(jù)和行為的分離。
讓我們再來回顧一下包含靜態(tài)類型的calculateAreas方法:
const calculateAreas = (radii: Array<number>): Array<number> => {
var areas = [];
for (var i = 0; i < radii.length; i++) {
areas[i] = 3.14 * (radii[i] * radii[i]);
}
return areas;
};
思考一下我們是如何來編寫、設(shè)計這個方法的。由于我們聲明了類型,我們必須先考慮我們打算要使用的數(shù)據(jù)類型,以便于我們可以大體定義好輸入與輸出的類型。

這之后我們才會去填充具體的邏輯實現(xiàn)部分:

這個能力恰恰表明了,數(shù)據(jù)和行為的分離是我們更明確我們的腦海中對方法的設(shè)想,同時也更準確地傳達我們的意圖,這種方式減輕了無關(guān)的思維負擔,使我們對程序的思考更加純粹與清晰。(譯者:這應該是程序設(shè)計時對我們思維方式的一種提示、引導與規(guī)范)
優(yōu)勢六:幫助我們消除了一整類bug
作為JavaScript開發(fā)者,我們最常遇見的錯誤就是運行時的類型錯誤。
舉個例子,應用程序最初是的狀態(tài)被定義為:
var appState = {
isFetching: false,
messages: [],
};
假設(shè)接下來我們創(chuàng)建了一個用來獲取messages數(shù)據(jù)的API。然后,我們的app有一個簡單的組件會將messages(在state中定義的)作為屬性來裝載,并且顯示未讀數(shù)量和messages的列表:
import Message from './Message';
const MyComponent = ({ messages }) => {
return (
<div>
<h1> You have { messages.length } unread messages </h1>
{ messages.map(message => <Message message={ message } /> )}
</div>
);
};
如果這個獲取messages數(shù)據(jù)的API失敗了或是返回了undefined,在生產(chǎn)環(huán)境中這段程序就會出現(xiàn)一個類型錯誤:
TypeError: Cannot read property ‘length’ of undefined
……然后我們的程序就崩潰了。你就這么失去了你的客戶。僵……
讓我們看看類型檢查在這有什么作用。首先是給應用的state添加類型。我會使用類型別名的功能,創(chuàng)建一個AppState類型,然后用它來定義state:
type AppState = {
isFetching: boolean,
messages: ?Array<string>
};
var appState: AppState = {
isFetching: false,
messages: null,
};
由于知道了獲取messages的API并不可靠,因此在這里我給messages定義了一個為字符串數(shù)組的maybe類型。
最后同樣的,我們在view層組件中獲取messages,并將它渲染在組件中。
import Message from './Message';
const MyComponent = ({ messages }) => {
return (
<div>
<h1> You have { messages.length } unread messages </h1>
{ messages.map(message => <Message message={ message } /> )}
</div>
);
};
Flow會幫我們捕獲并將錯誤告知我們:
<h1> You have {messages.length} unread messages </h1>
^^^^^^ property `length`. Property cannot be accessed on possibly null value
<h1> You have {messages.length} unread messages </h1>
^^^^^^^^ null
<h1> You have {messages.length} unread messages </h1>
^^^^^^ property `length`. Property cannot be accessed on possibly undefined value
<h1> You have {messages.length} unread messages </h1>
^^^^^^^^ undefined
{ messages.map(message => <Message message={ message } /> )}
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ call of method `map`. Method cannot be called on possibly null value
{ messages.map(message => <Message message={ message } /> )}
^^^^^^^^ null
{ messages.map(message => <Message message={ message } /> )}
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ call of method `map`. Method cannot be called on possibly undefined value
{ messages.map(message => <Message message={ message } /> )}
^^^^^^^^ undefined
Wow!
由于我們將messages定義為maybe類型,因此messages可以是null或者undefined。但在沒有進行空檢查的情況下,我們?nèi)匀徊荒茉谒厦孢M行一些操作(像是.length或.map)。因為如果messages的值是null或者undefined的話,而我們又在它上面進行了相關(guān)操作,程序同樣會報一個類型錯誤。
因此我們回頭去更新一下view層的代碼:
const MyComponent = ({ messages, isFetching }: AppState) => {
if (isFetching) {
return <div> Loading... </div>
} else if (messages === null || messages === undefined) {
return <div> Failed to load messages. Try again. </div>
} else {
return (
<div>
<h1> You have { messages.length } unread messages </h1>
{ messages.map(message => <Message message={ message } /> )}
</div>
);
}
};
現(xiàn)在Flow知道我們已經(jīng)對null或者undefined的情況進行了處理,因此類型檢查器不會報任何錯誤。當然,運行時的類型錯誤也就不會再來煩你啦。
優(yōu)勢七:減少單元測試的數(shù)量
在之前的部分我們已經(jīng)知道了,靜態(tài)類型是如何通過確保輸入與輸出的類型來幫助消除復雜的錯誤處理。因此,它也可以幫助我們減少單元測試的數(shù)量。
例如,在包含了錯誤處理的動態(tài)類型方法calculateAreas中:
const calculateAreas = (radii) => {
// Handle undefined or null input
if (!radii) {
throw new Error("Argument is missing");
}
// Handle non-array inputs
if (!Array.isArray(radii)) {
throw new Error("Argument must be an array");
}
var areas = [];
for (var i = 0; i < radii.length; i++) {
if (typeof radii[i] !== "number") {
throw new Error("Array must contain valid numbers only");
} else {
areas[i] = 3.14 * (radii[i] * radii[i]);
}
}
return areas;
};
如果你是一個勤勞的程序員,為了確保輸入?yún)?shù)在程序中處理正確,也許你會想對輸入?yún)?shù)的有效性添加測試:
it('should not work - case 1', () => {
expect(() => calculateAreas([null, 1.2])).to.throw(Error);
});
it('should not work - case 2', () => {
expect(() => calculateAreas(undefined).to.throw(Error);
});
it('should not work - case 2', () => {
expect(() => calculateAreas('hello')).to.throw(Error);
});
等等一系列測試。此外,我們很可能會忘了添加一下邊緣用例的測試,而恰好我們的用戶發(fā)現(xiàn)了它。
由于這些測試是基于我們所有想到的用例,因此就存在一些很容易被忽略的。
在另一方面,當我們需要去定義一些類型:
const calculateAreas = (radii: Array<number>): Array<number> => {
var areas = [];
for (var i = 0; i < radii.length; i++) {
areas[i] = 3.14 * (radii[i] * radii[i]);
}
return areas;
};
不僅僅是確保了代碼實際運行的結(jié)果和我們的意圖一致,也使得它們不容易被我們漏掉。不同于以經(jīng)驗為主的測試,類型檢查是通用的。
這個場景的意思是:在邏輯測試中,測試是很不錯的;而在數(shù)據(jù)類型的測試中,類型檢查則較好。將兩者結(jié)合可以起到一加一大于二的效果。
優(yōu)勢八:提供了領(lǐng)域建模(domain modeling)工具
關(guān)于類型檢查,我最喜歡的一個應用場景是領(lǐng)域建模。領(lǐng)域模型是,一個同時包括了數(shù)據(jù)和基于該數(shù)據(jù)的行為的該領(lǐng)域的概念模型。
A domain model is a conceptual model of a domain that includes both the data and behavior on that data.
理解如何使用類型來進行領(lǐng)域建模的最好的方法就是看一個例子。
假設(shè)現(xiàn)在有一個應用,用戶在這個平臺上有一種或多種購買商品的支付方式?,F(xiàn)在Paypal、信用卡和銀行賬戶這三種方式是可供選擇的。
我們首先給這三種支付方式創(chuàng)建相應的類型別名:
type Paypal = { id: number, type: 'Paypal' };
type CreditCard = { id: number, type: 'CreditCard' };
type Bank = { id: number, type: 'Bank' };
現(xiàn)在我們可以用這三種情況和或運算符來定義我們的PaymentMethod類型:
type PaymentMethod = Paypal | CreditCard | Bank;
這樣足夠了么?好吧,我們知道,當我們使用支付方法時,我們需要請求一個API,并且,根據(jù)我們所處的不同的請求階段,我們的app的狀態(tài)也是不同的。因此,會有四種可能的狀態(tài):
- 我們已經(jīng)獲取到了支付方法
- 我們正在獲取支付方法
- 我們成功獲取了支付方法
- 我們嘗試去獲取了支付方法但是出現(xiàn)了錯誤
但是我們上面這個簡單的paymentMethods類型的模型并沒有覆蓋所有的場景。而是假設(shè)paymentMethods是始終存在的。
有什么辦法能給我們的應用狀態(tài)建一個模型,使它居且僅居這四個狀態(tài)之一么?讓我們來看一下:
type AppState<E, D>
= { type: 'NotFetched' }
| { type: 'Fetching' }
| { type: 'Failure', error: E }
| { type: 'Success', paymentMethods: Array<D> };
我們使用了“|”或運算符來定義我們的應用狀態(tài)是上面描述的這四種中的一種。注意我是怎么使用type屬性來確定app的狀態(tài)是四個中的哪一個的。使用這種方式,我們就可以分析確定什么時候我們獲取到了支付方法,而是什么時候沒有。
你還會注意到,我給應用狀態(tài)傳入的泛型E和D。類型D代表了用戶的支付方法(之前定義的PaymentMethod)。我們沒有定義類型E,這會是我們的錯誤類型(error type)。下面我們可以這么做:
type HttpError = { id: string, message: string };
總得來說,現(xiàn)在我們的應用狀態(tài)的標識是AppState<E, D>——E是HttpError類型,D是PaymentMethod支付方式。并且AppState有四個可能(且只有這四種)的狀態(tài):NotFetched、Fetching、Failure和Success。

我發(fā)現(xiàn)這種領(lǐng)域建模的類型對我們思考user interfaces的構(gòu)建而不是特定的業(yè)務邏輯很有幫助。業(yè)務規(guī)則告訴我們,我們的應用只會是這些狀態(tài)中的一個。這使得我們可以明確地構(gòu)建應用的狀態(tài),并且保證其只會是預先定義的狀態(tài)中的一種。當我們構(gòu)建了這樣一個模型(例如一個view層組件),很明顯,我們需要去處理所有這四種可能的狀態(tài)。
更進一步地,這段代碼變得很清晰(self-documenting),你可以根據(jù)這些可能的場景立即搞清楚應用狀態(tài)的結(jié)構(gòu)。
使用靜態(tài)類型的劣勢
正如生活與編程中的其他東西,靜態(tài)類型檢查也有著它的利弊取舍。
重要的一點是,我們學習并理解靜態(tài)類型,這樣就可以對何時靜態(tài)類型是有意義的與何時它們并沒那么有價值的問題有一個明智的判斷。
下面就是一些相關(guān)的思考:
劣勢1:需要預先學習靜態(tài)類型相關(guān)知識
對初學者來說,JavaScript真的是一門不錯的語言,原因之一就是在寫代碼之前你不需要去了解完整的類型系統(tǒng)。
當我最開始學習Elm(一門靜態(tài)類型語言),類型相關(guān)的問題經(jīng)常會出現(xiàn),我總是會寫出一些類型定義相關(guān)的編譯錯誤。
有效的學習一門語言的類型系統(tǒng)可以說和學習語言本身一樣都是一場硬仗。因此,靜態(tài)類型使得Elm的學習曲線比JavaScript要陡峭。
尤其是對于一些初學者,學習語法本身就是一件高負荷的事情了。在去混雜著學習類型會搞垮他們。
劣勢2:代碼的冗長會讓你頭疼
靜態(tài)類型經(jīng)常會讓代碼看起來更冗長和雜亂。
例如,我們要替換下面這段代碼:
async function amountExceedsPurchaseLimit(amount, getPurchaseLimit){
var limit = await getPurchaseLimit();
return limit > amount;
}
我們需要這么寫:
async function amountExceedsPurchaseLimit(
amount: number,
getPurchaseLimit: () => Promise<number>
): Promise<boolean> {
var limit = await getPurchaseLimit();
return limit > amount;
}
在譬如,替換下面這段:
var user = {
id: 123456,
name: 'Preethi',
city: 'San Francisco',
};
需要寫成:
type User = {
id: number,
name: string,
city: string,
};
var user: User = {
id: 123456,
name: 'Preethi',
city: 'San Francisco',
};
顯然,我們添加了一些額外的代碼。但是,有幾個觀點并不認為這是不好的。
首先,正如我們之前所提到的,靜態(tài)類型幫助我們消除了一整類測試。一些開發(fā)者會認為這是一個很合理的這兩方面的權(quán)衡。
其次,正如之前看到的,靜態(tài)類型有的時候可以消除復雜的錯誤處理,反而可以較大的減少代碼的雜亂性。
很難說靜態(tài)類型是否真的讓代碼變得冗長了,但我們可以在實際工作中保留對這個問題的思考。
劣勢3:需要花時間去掌握類型
學會怎么最好地在程序里定義類型需要花很多時間來進行實踐。而且,建立一個良好的意識——何時使用靜態(tài)類型、何時使用動態(tài)類型,也需要認真的思考和豐富的實踐經(jīng)驗。
例如,我們可能采取的一種方式是,對關(guān)鍵的業(yè)務邏輯使用類型,而對臨時性的、不重要的邏輯部分使用動態(tài)類型來降低不必要的復雜度。
這還是很難區(qū)分的,尤其是當開發(fā)者對類型使用的經(jīng)驗較少時。
劣勢4:靜態(tài)類型可能會延緩一些開發(fā)速度
正如前文提到的,當我學習Elm時,學習使用類型給我?guī)砹诵┬∽璧K,尤其是當我需要加一些或改一些代碼時。經(jīng)常出現(xiàn)編譯錯誤讓我心煩意亂,很難感覺到自己的進步。
有人認為,靜態(tài)類型檢查在絕大多數(shù)情況下,可能會導致程序員精力不集中,而我們知道,集中精神是寫出好代碼的關(guān)鍵。
不僅僅如此,靜態(tài)類型檢查器也不總是完美的。有時候你知道自己需要怎么做而類型檢查器反而成為了絆腳石。
我相信還有一些其他需要取舍的地方,但上面這幾條是對我來說很重要的。
下一部分,最終的結(jié)論
在最后一部分,通過討論使用靜態(tài)類型是否有意義,進行總結(jié)。
原文:Why use static types in JavaScript? The Advantages and Disadvantages)