TypeScript 被吹過頭了

開始看本文之前,我希望讀者朋友們知道我在很大程度上是一位 TypeScript 粉絲。在我的前端 React 項目和各種後端 Node 工作裡,所使用的主要編程語言都是 TypeScript。我是這條船上的人,但也確實有一些疑惑,想在這篇文章中討論一下。到目前為止,我已經使用 TypeScript 寫了至少三年的代碼,涉及的項目不計其數,因此可以說 TypeScript 的確是走在了正路上,或者說滿足了某種需求。

TypeScript 克服了一些難以逾越的障礙,成為了前端編程領域的主流之選。TypeScript 在這篇文章列出的最受歡迎編程語言中排名第七。

無論你是否在使用 TypeScript,不管多大規模的軟件團隊都應遵循以下實踐:

  • 精心編寫的單元測試應在合理範圍內覆蓋儘可能多的生產代碼;
  • 結對編程——另一雙眼睛可以捕捉到的遠不止語法錯誤;
  • 良好的同行評審流程——正確的同行評審可以找出許多機器無法捕獲的錯誤;
  • 使用 eslint 之類的 linter。

TypeScript 可以在這些基礎之上增加額外的一層安全性,但我認為這在編程語言的需求列表中是排在非常靠後的位置上的。

TypeScript 並非健全的類型系統

我認為這可能是 TypeScript 當前版本面臨的主要問題,但首先,我來定義一下什麼是健全(sound)和不健全(unsound)的類型系統。

健全

一個健全的類型系統能確保你的程序不會進入無效狀態。例如,如果一個表達式的靜態類型是 string,則在運行時,對它求值時你肯定只會得到一個 string。

在健全的類型系統中,永遠不會在編譯時 或運行時 出現表達式與預期類型不匹配的情況。

當然,健全性(soundness) 也是有級別的,具體含義可以斟酌。TypeScript 具有一定程度的健全性,並會捕獲以下類型的錯誤:

<code>// 類型 'string' 不能賦值給 類型 'number'const increment = (i: number): number => { return i + "1"; } // 類型 '"98765432"' 的參數不能賦值給類型 'number' 的參數.const countdown: number = increment("98765432");/<code>

不健全

關於 Typescript,以下事實是非常明確的:100%的健全性不是它的目標,並且 TypeScript 的非目標列表中的 3 號非目標明確指出:

應用一個健全或“正確無誤”的類型系統(不是我們的目標)。相反,要在正確性和生產力之間取得平衡。

這意味著不能保證變量在運行時具有定義的類型。我可以用下面一些人為的例子說明這一點:

<code>interface A {    x: number;} let a: A = {x: 3}let b: {x: number | string} = a;b.x = "unsound";let x: number = a.x; // 不健全 a.x.toFixed(0); // 這啥?/<code>

上面的代碼是不正確的,因為從 A 接口中可知 a.x 應該是一個數字。不幸的是,經過一些重新賦值後,它最終以一個字符串的形式出現,並且後面的代碼能通過編譯,但會在運行時出錯。很不幸,這裡顯示的表達式可以正確編譯:

<code>a.x.toFixed(0);/<code>

我認為這可能是 TypeScript 的最大問題,那就是健全性不是它的目標。我仍會遇到許多運行時錯誤,這些錯誤沒有被 tsc 編譯器標記出來,但如果 TypeScript 有一個健全的系統就能做到了。採用這種方法,意味著 TypeScript 在健全和不健全的陣營之間是在腳踩兩隻船。這種搖擺路線遇上 any 類型就更嚴重了,我將在後文提到這一點。我還是得編寫儘可能多的測試,這讓我感到沮喪不已。當我第一次開始使用 TypeScript 時,我錯誤地得出了一個結論,以為以後就用不著編寫這麼多單元測試了。

TypeScript 挑戰了現狀,聲稱降低用戶使用類型的認知開銷比類型健全更重要。

我理解為什麼 TypesScript 會走這條路,並且有一個觀點指出,如果類型系統的健全性得到 100%保證,那麼 TypeScript 的採用率就不會那麼高了。隨著 dart 語言逐漸流行(因為 Flutter 現已廣泛應用),這個看法也被證偽了。健全性是 dart 語言的一個目標,可以參考這裡的討論。

TypeScript 的不健全性,以及它在嚴格的類型系統上開的一大堆天窗拖累了它的效率,讓它在今天處於相當 雞肋 的位置上,非常可惜。我的願望是,隨著 TypeScript 的流行,會有更多的編譯器選項可供使用,從而使高級用戶可以爭取 100%的健全性。

TypeScript 不保證任何運行時類型檢查

運行時類型檢查不是 TypeScript 的目標之一,因此這種願望可能永遠不會實現。例如,在處理從 API 調用返回的 JSON 負載時,運行時類型檢查會很有用。如果我們可以在類型級別上控制它,就用不著一整筐錯誤和許多單元測試了。

我們無法在運行時保證任何事情,因此可能會出現以下情況:

<code>const getFullName = async (): string => {  const person: AxiosResponse = await api();   //response.name.fullName 可能在運行時成為 undefined  return response.name.fullName}/<code>

有一些支持這種需求的庫,例如 io-ts;雖然它很棒,但可能意味著你必須複製你的模型。

可怕的 any 類型和嚴格選項

any 類型就是字面意思,編譯器允許任何操作或賦值。

TypeScript 往往在小事上表現很不錯,但是人們習慣給花費時間超過 1 分鐘的任何事物都來個 any 類型。我最近在一個 Angular 項目中工作,看到很多這樣的代碼:

<code>export class Person { public _id: any; public name: any; public icon: any;/<code>

TypeScript 會讓你忘掉類型系統。你可以用一個 any 把所有事物的類型幹掉:

<code>("oh my goodness" as any).ToFixed(1); // 記得我提到健全性時說的話嗎?/<code>

strict 編譯器選項會啟用以下編譯器設置,這些設置會讓事物看起來更健全:

  • –strictNullChecks
  • –noImplicitAny
  • –noImplicitThis
  • –alwaysStrict

還有 eslint 規則 @typescript-eslint/no-explicit-any。

any 的擴散會毀掉你類型系統的健全性。

結論

我必須重申一點,我是 TypeScript 粉絲,也在日常工作中使用它,但我確實認為它有很多缺陷,而且炒作有些過頭了。Airbnb 聲稱 TypeScript 可以阻止 38%的錯誤。我非常懷疑這一精確的百分比數字。TypeScript 不會給已有的良好實踐錦上添花。我還是得編寫儘可能多的測試。你可能不相信,我編寫的代碼變多了,可能還得編寫許多類型測試。我仍然會遇到意外的運行時錯誤。

TypeScript 提供了比較基礎的類型檢查,但健全性和運行時類型檢查不是它的目標,這讓 TypeScript 處於腳踩兩條船的狀態,一隻腳踏上了更好的那艘,一隻腳還呆在現在的破船上,真是不幸。

TypeScript 的亮點在於良好的 IDE 支持,比如說 vscode 裡如果我們打錯什麼內容,就會獲得視覺反饋。

TypeScript 被吹過頭了

vscode 中的 TypeScript 錯誤

使用 TypeScript 還可以增強重構能力,並且在對修改後的代碼運行 TypeScript 編譯器時,可以立即識別出代碼中斷(例如方法簽名的更改)。

TypeScript 帶來了良好的類型檢查,並且絕對比沒有類型檢查器或僅使用普通的 eslint 要更好,但是我認為它能更進一步。另外對於我們這種想要更多能力的用戶來說,它應該可以提供足夠多的編譯器選項才是。

關注我並轉發此篇文章,私信我“領取資料”,即可免費獲得InfoQ價值4999元迷你書!


分享到:


相關文章: