作者 | 微軟官方博客
譯者 | 核子可樂
策劃 | 小智
稿源?|?前端之巔
今天,微軟在其官方博客宣布:TypeScript 3.9 版本已經正式發布,詳情見下文。
有些朋友可能對 TypeScript 還不太熟悉,這是一種以 JavaScript 為基礎開發的語言,新增 type 聲明與注釋等多種語法。TypeScript 編譯器能夠使用這些語法對代碼進行 type 檢查,而后輸出能夠適配多種不同運行時、且清晰可讀的 JavaScript 代碼。
由于 TypeScript 具有豐富的跨編輯器功能,因此其中的靜態 type 檢查能夠在代碼運行甚至文件保存之前快速指示代碼中存在的錯誤。除了錯誤檢查之外,TypeScript 還允許用戶在自己熟悉的編輯器中為 TypeScript 以及 JavaScript 代碼提供補全、快速修復以及重構等功能。事實上,如果你曾經使用過 Visual Studio 或者 Visual Studio Code,那么以往的 JavaScript 代碼編寫體驗中可能就已經有 TypeScript 的貢獻了。如果希望了解更多詳細信息,請 訪問我們的網站。
而如果你已經在項目當中使用過 TypeScript,那么直接通過以下 npm 命令或者通過 NuGet 都能快速獲取我們發布的本次新版本:
npm?install?typescript
當然,你還可以通過以下方式獲取編輯器支持:
下載 Visual Studio 2019/2017 對應版本;
https://marketplace.visualstudio.com/items?itemName=TypeScriptTeam.TypeScript-39
安裝 Visual Studio Code Insiders 版本;
http://code.visualstudio.com/insiders
或者通過鏈接使用 TypeScript 新版本;
https://code.visualstudio.com/docs/typescript/typescript-compiling\l_using-newer-typescript-versions
配合 Sublime Text 3 使用 PackageControl。
https://packagecontrol.io/packages/TypeScript
在此次新版本中,我們的團隊高度關注性能表現、細節處理與穩定性。我們一直努力提高編譯器速度與編輯體驗,擺脫卡頓與繁瑣的細節,同時減少 bug 與系統崩潰問題。當然,我們也從外部社區收到了很多有價值的功能與修復貢獻。
Inference 與 Promise.all 迎來改進
TypeScript 的最近幾個版本(3.7 及之后)已經對 Promise.all 及 Promise.race 等函數的聲明做出更新。遺憾的是,更新帶來了新的問題,這一點在混合 null 或 undefined 值時體現得尤其明顯。
interface?Lion {roar():?void}interface?Seal {singKissFromARose():?void}async?function?visitZoo(lionExhibit:?Promise<Lion>, sealExhibit:?Promise<Seal |?undefined>)?{let?[lion, seal] =?await?Promise.all([lionExhibit, sealExhibit]);lion.roar();?// uh oh// ~~~~// Object is possibly 'undefined'.}
這種情況非常奇怪!事實上,sealExhibit 當中包含的 undefined,相當于是把 undefined 錯誤引入了 lion type 當中。
感謝 Jack Bates 提交的貢獻,現在這個問題已經在 TypeScript 3.9 版本中得到修復。以上錯誤不復存在,如果大家仍在較早版本的 TypeScript 面臨 Promise 的困擾,我們建議您盡快升級至 3.9 版本!
關于 awaited ?type 的變化
如果大家一直在關注我們的問題跟蹤器與設計研討記錄,可能已經注意到我們正在開發一種名為 awaited 的全新 type 操作符。該操作符的作用是準確對 JavaScript 中的 Promise 展開方式進行建模。
我們最初預計在 TypeScript 3.9 版本中發布 awaited,但在使用現有代碼庫運行早期 TypeScript build 時,我們意識到這項功能還需要進一步打磨才能正式交付。因此,我們決定將該功能從主分支中剝離出來,直到其做好服務用戶的一切準備。我們將對這項功能進行更多試驗,因此在 3.9 版本中 awaited 將暫時制度。
速度改進
TypeScript 3.9 將帶來一系列新的速度改進機制。在發現 Material-ui 與 Styled-Components 等組件會帶來極差的編輯 / 編譯速度后,我們的團隊一直努力進行性能優化。我們在這方面進行了深入研究,并提交多項 pull 請求以優化涉及大型聯合、交集點、條件 type 以及映射 type 的性能問題。
-
https://github.com/microsoft/TypeScript/pull/36576
-
https://github.com/microsoft/TypeScript/pull/36590
-
https://github.com/microsoft/TypeScript/pull/36607
-
https://github.com/microsoft/TypeScript/pull/36622
-
https://github.com/microsoft/TypeScript/pull/36754
-
https://github.com/microsoft/TypeScript/pull/36696
在部分代碼庫上,相關 pull 請求的編譯時間平均減少了 5% 至 10%。總體而言,我們已經將 material-ui-styles 項目的編譯時間縮短約 25%。此外,我們還收到來自微軟團隊的反饋意見,他們表示 TypeScript 3.9 的平均編譯時長由 26 秒縮短至 10 秒左右。
我們還對編輯器方案中的文件重命名功能做出幾項調整。根據 Visual Studio Code 團隊提供的建議,我們發現在執行文件重命名時,單是查明哪些導入語句需要更新就要耗去 5 到 10 秒時間。TypeScript 3.9 調整了內部編譯器與語言服務緩存文件的查找方式,順利解決了這個問題。
雖然仍有改善空間,但我們希望目前的成果能夠為廣大用戶帶來更好的使用體驗!
// @ts-expect-error 注釋
想象一下,如果我們正使用 TypeScript 編寫一個庫,并將名為 doSTuff 的函數作為公共 API 的一部分進行導出。該函數的 type 聲明需要兩個 strings,以便其他 TypeScript 用戶正常獲取 type-checking 錯誤。但與此同時,它還需要執行運行時檢查(可能僅在開發 build 中)以向 JavaScript 用戶提示錯誤信息。
function?doStuff(abc:?string, xyz:?string)?{assert(typeof?abc ===?"string");assert(typeof?xyz ===?"string");// do some stuff}
因此一旦發生操作失誤,TypeScript 用戶面對的將是一條標紅的亂碼信息外加一條錯誤信息。而 JavaScript 用戶則面對一條斷言錯誤。我們希望通過單元測試檢查實際情況與預期是否相符。
expect(()?=>?{doStuff(123,?456);}).toThrow();
遺憾的是,我們的測試是由 TypeScript 編寫而成,而 TypeScript 只能提示一條錯誤信息!
doStuff(123,?456);//??????????~~~//?error:?Type?'number'?is?not?assignable to type?'string'.
為此,TypeScript 3.9 帶來了新功能:// @ts-expect-error 注釋。在一行代碼以 // @ts-expect-error 注釋作為前綴時,TypeScript 會禁止報告該錯誤。而如果沒有發生錯誤,TypeScript 則報告不需要 // @ts-expect-error。
在以下簡單示例代碼中,一切正常運行:
//?@ts-expect-errorconsole.log(47?*?"octopus");
但下列代碼:
//?@ts-expect-errorconsole.log(1?+?1);
會導致錯誤:
Unused?'@ts-expect-error'?directive.
我們要特別感謝此項功能的貢獻者 Josh Goldberg。關于更多詳細信息,請參閱 ts-expect-error pull 請求:
https://github.com/microsoft/TypeScript/pull/36014
ts-ignore 還是 ts-expect-error?
在某種程度上講,// @ts-expect-error 可以作為抑制注釋使用,其效果類似于 // @ts-ignore。但二者的區別在于,如果下一行代碼沒有錯誤,則 // @ts-ignore 不會發揮任何作用。
大家可能打算把現有 // @ts-ignore 注釋變更為 // @ts-expect-error,而且好奇哪種方法更適合用于后續代碼編寫。雖然具體選擇取決于您和您的團隊,但這里我們還是整理出了一些相對普適的選擇思路。
如果符合以下條件,請選擇 ts-expect-error :
-
您正在編寫測試代碼,且希望 type 系統在單一操作上顯示錯誤。
-
您希望盡快獲得修復方法,只要能解決問題就行。
-
您的項目規模合理,團隊工作態度積極主動,希望在受影響代碼恢復正常之后馬上刪除抑制注釋。
如果符合以下條件,請選擇 ts-ignore :
-
您的項目規模很大大,而且在缺少明確歸屬的代碼中出現了新錯誤。
-
您正在兩種不同 TypeScript 版本之間升級,某行代碼只在其中一個版本上出現了錯誤。
-
您根本沒有時間認真考慮這兩個選項中哪個更好。
在條件表達式中檢查未調用函數
在 TypeScript 3.7 版本中,我們引入了未調用函數檢查(uncalled function checks)以提示那些您忘記調用的函數。
function?hasImportantPermissions():?boolean?{// ...}// Oops!if?(hasImportantPermissions) {// ~~~~~~~~~~~~~~~~~~~~~~~
// This condition will always return true since the function is always defined.
// Did you mean to call it instead?deleteAllTheImportantFiles();}
然而,這種錯誤只適用于 if 語句。感謝 Alexander Tarasyuk 的貢獻,現在此項功能已經能夠正常支持三種條件(即 cond ? trueExpr : falseExpr 語法)。
declare?function?listFilesOfDirectory(dirPath:?string):?string[]; declare?function?isDirectory():?boolean; function?getAllFiles(startFileName:?string)?{const?result:?string[] = [];traverse(startFileName);return?result;
????function?traverse(currentPath:?string)?{return?isDirectory ?// ~~~~~~~~~~~// This condition will always return true// since the function is always defined.// Did you mean to call it instead?listFilesOfDirectory(currentPath).forEach(traverse) :result.push(currentPath);}}
Alexander 還進一步提交了快速修復方案,旨在改善未調用函數檢查功能的使用體驗!
編輯器改進
TypeScript 編譯器不只增強了大部分主流編輯器中的 TypeScript 編輯體驗,同時也增強了 Visual Studio 系列編輯器中的 JavaScript 開發體驗。根據您所使用的具體編輯器,新的 TypeScript/JavaScript 功能也會有所不同。以下為幾項共通性改進:
-
Visual Studio Code 現在允許您選擇不同的 TypeScript 版本。此外,JavaScript/TypeScript Nightly Extension 也將始終保持最新(通常相當穩定)。
-
Visual Studio 2017/2019 迎來最新版本的 SDK 安裝器與 MSBuild 安裝程序。
-
Sublime Text 3 支持用戶選擇不同 TypeScript 版本。
JavaScript 中的 CommonJS 自動補全
新版本的另一項重大改進,是使用 CommonJS 模塊自動導入 JavaScript 文件。
在舊版本中,TypeScript 強制要求用戶無論使用什么文件,都必須以 ECMAScript 的形式導入,例如:
import?*?as?fs?from?"fs";
但在編寫 JavaScript 文件時,很多用戶并不打算使用 ECMScript 樣式模塊。不少朋友仍在使用 CommonJS 樣式的 require(...) 導入,例如:
const?fs =?require("fs");
TypeScript 現在能夠自動檢測您所使用的導入類型,保證文件樣式簡潔而統一。
關于更多詳細信息,請參閱相應 pull 請求:
https://github.com/microsoft/TypeScript/pull/37027
代碼操作保留換行符
TypeScript 的重構與快速修復往往無法正確保留換行符。先來看以下簡單代碼示例:
const?maxValue =?100;/*start*/for?(let?i =?0; i <= maxValue; i++) {// First get the squared value.let?square = i **?2;// Now print the squared value.console.log(square);}/*end*/
如果我們在編輯器中從 /*start*/ 到 /*end*/ 的高亮顯示區域內提取一條新函數,則最終得出的代碼將如下所示:
const?maxValue =?100;printSquares();function?printSquares()?{for?(let?i =?0; i <= maxValue; i++) {// First get the squared value.let?square = i **?2;// Now print the squared value.console.log(square);}}
這就不對了——原本 for 循環中的每個語句間都有一個空白行,但重構之后空白行消失了!好消息是,TypeScript 在保持編寫內容準確性方面做出不少改進。
const?maxValue =?100;printSquares();function?printSquares()?{for?(let?i =?0; i <= maxValue; i++) {// First get the squared value.let?square = i **?2;// Now print the squared value.console.log(square);}}
關于更多詳細信息,請參閱相應 pull 請求:
https://github.com/microsoft/TypeScript/pull/36688
快速修復缺失的返回表達式
在某些情況下,大家很可能會忘記返回函數中最后一條語句的值。這種情況在向箭頭函數添加大括號時體現得尤其明顯。
// beforelet?f1 =?()?=>?42// oops - not the same!let?f2 =?()?=>?{?42?}
感謝社區成員 Wenlu Wang 的貢獻,TypeScript 現在獲得了快速修復功能,可添加缺失的 return 語句、刪除大括號或者為對象字面量等箭頭函數實體添加括號。
支持 “Solution Style” tsconfig.json 文件
編輯器需要確定當前文件屬于哪個配置文件,以及當前“項目”中還包含哪些其他文件,從而選擇適當的選項。在默認情況下,由 TypeScript 語言服務器支持的編輯器會在各個父目錄中查找 tsconfig.json 以實現這一目的。
但問題在于,某些簡單 tsconfig.json 會直接引用其他 tsconfig.json 文件。
// tsconfig.json{"files": [],"references": [{?"path":?"./tsconfig.shared.json"?},{?"path":?"./tsconfig.frontend.json"?},{?"path":?"./tsconfig.backend.json"?},]}
換句話說,這個文件的作用只是管理其他項目文件;在某些環境中,我們將這類文件稱為“solution”。很明顯,服務器無法正確提取這些 tsconfig.*.json 文件,但我們的目標正是讓語言服務器意識到當前.ts 文件可能歸屬于 tsconfig.json 根目錄所提及的其他項目。
TypeScript 3.9 解決了這個支持問題。關于更多詳細信息,請參閱 相應的 pull 請求。
重大變化
解析可選鏈與非 null 斷言中的差異
TypeScript 最近實現了對可選鏈操作符的支持,但根據用戶反饋,非 null 斷言操作符(!)的可選鏈(?.)行為不符合直覺。
具體來講,在以往的版本中,代碼:
foo?.bar!.baz
被解釋為等效于以下 JavaScript 代碼:
(foo?.bar).baz
在以上代碼中,括號會阻止可選鏈的“短路”行為;因此如果未定義 foo 為 undefined,則訪問 baz 會引發運行時錯誤。
發現這一問題的 Babel 團隊以及向我們提交反饋的大部分其他用戶,都認為這樣的行為屬于設計失誤。我們完全認同大家的看法!根據群眾和我們自己的內部意見,由于操作目的是從 bar type 中刪除 null 與 undefined,因此!操作符應該直接“消失”。
換句話說,大多數人認為以上原始代碼片段應該被解釋為在:
foo?.bar.baz
中,當 foo 為 undefined 時,計算結果為 undefined。
這是一項重大變化,但我們認為大部分代碼在編寫時都是為了考慮新的解釋場景。如果您希望繼續使用舊有行為,則可在!操作符左側添加括號,如下所示:
(foo?.bar)!.baz
} 與 > 現在為無效的 JSX 文本字符
JSX 規范禁止在文本位置中使用}與>字符,TypeScript 與 Babel 也遵循相同的規則。要在新版本中插入這些字符,您需要使用 HTML 轉義代碼 (例如 <div>2 >?1</div>?) 或者插入一個帶有字符串字面值的表達式 (例如 <div>2 {">"} 1</div>?)。
幸運的是,由于 Brad Zacher 提交的 pull 請求,現在直接使用這兩個符號會彈出以下錯誤提示:
Unexpected token. Did you mean `{'>'}`?or?`>`?Unexpected token. Did you mean `{'}'}`?or?`}`?
例如:
let?directions =?<div>Navigate to: Menu Bar > Tools > Options</div>// ~ ~// Unexpected token. Did you mean `{'>'}` or `>`?
這條錯誤消息中還附帶便捷的快速修復功能,感謝 Alexander Tarasyuk 的貢獻,您的努力讓批量處理錯誤修復變得非常輕松。
更嚴格地檢查交集與可選屬性
一般來說,如果 A 和 B 中的任何一個可被賦值給 C,那么像 A 與 B 這樣的交集 type 就可以被賦值給 C;但有時候,可選屬性會引發問題。例如:
interface?A {a:?number;?// notice this is 'number'}interface?B {b:?string;}interface?C {a?:?boolean;?// notice this is 'boolean'b:?string;}declare?let?x: A & B;
declare?let?y: C;y = x;
在之前的 TypeScript 版本中,上述代碼能夠正常運行,因為 A 與 C 完全不兼容,而 B 與 C 兼容。
在 TypeScript 3.9 中,只要交集中的每個 type 都是一個具體的對象 type,則 type 系統將同時考慮所有屬性。因此,TypeScript 會意識到 A&B 中的 A 屬性與 C 不兼容:
Type?'A & B'?is?not assignable?to?type?'C'.Types of property?'a'?are incompatible.Type?'number'?is?not assignable?to?type?'boolean | undefined'.
若需了解更多詳細信息,請參閱相應 pull 請求:
https://github.com/microsoft/TypeScript/pull/37195
通過屬性判斷減少交集
在某些情況下,我們的 type 可能會描述并不存在的值,例如:
declare?function?smushObjects<T,?U>(x: T, y: U):?T?&?U;interface?Circle {kind:?"circle";radius:?number;}interface?Square {kind:?"square";sideLength:?number;}declare?let?x: Circle;
declare?let?y: Square;
let?z = smushObjects(x, y);console.log(z.kind);
這段代碼有點奇怪,因為我們實際上沒有辦法為 Circle 與 Square 創建出交集——二者擁有兩個互不兼容的 kind 字段。在之前的 TypeScript 版本中,這段代碼可以正常運行,只是 kind 本身由于 "circle" & "square" 描述的值集不可能存在而被解釋為 never 。
在 TypeScript 3.9 當中,type 系統變得更為嚴格——它會意識到 Circle 與 Square 因為 kind 屬性的不同而不可能存在交集。因此不同于舊版本將 z.kind type 折疊為 never,新版本會將 z type 本身(Circle & Square)折疊為 never。這意味著以上代碼現在將提示以下錯誤:
Property?'kind'?does not exist?on?type?'never'.
通過觀察,我們發現大多數中斷都由 type 聲明中的瑕疵引發。若需了解更多詳細信息,請參閱原始 pull 請求:
https://github.com/microsoft/TypeScript/pull/36696
Getters/Setters 不再屬于可枚舉屬性
在 TypeScript 舊版本中,類中的 get 與 set 訪問器會以可枚舉形式發出;但這明顯不符合 ECMAScript 規范。該規范要求將二者設定為不可枚舉屬性。因此,針對 ES5 與 ES2015 的 TypeScript 代碼可能在實際執行中引發不同的行為。
感謝 GitHub 用戶 pathurs 的貢獻,TypeScript 3.9 已經在這方面向 ECMAScript 的要求看齊。
擴展 any 的 Type 參數不再作為 any 執行
在 TypeScript 的舊版本中,受 any 約束的 type 參數可被視為 any。
function?foo<T?extends?any>(arg: T)?{arg.spfjgerijghoied;?// no error!}
這是一項明顯的疏忽,因此 TypeScript 3.9 采用了更保守的方法,將針對這些有問題的操作發出錯誤提示。
function?foo<T?extends?any>(arg: T)?{arg.spfjgerijghoied;// ~~~~~~~~~~~~~~~// Property 'spfjgerijghoied' does not exist on type 'T'.}
始終保留 export *
在此前的 TypeScript 版本當中,如果 foo 沒有導出任何值,則 export *?from "foo"這類聲明會在 JavaScript 輸出結果中被直接刪除。但這種處理方法并不完善,因為它是 type 定向的且無法被 Babel 模擬。TypeScript 3.9 將始終保留 export *?聲明。在實踐中,這項調整應該不會對代碼造成太多實際影響,但捆綁程序對代碼進行搖樹時難度可能會有所提升。
若需了解更多版本變化,請參閱原始 pull 請求:
https://github.com/microsoft/TypeScript/pull/37124
導出當前用戶 Getters 以實現活動綁定
當我們在 ES5 及以上版本中以 CommonJS 等模塊系統為目標時,TypeScript 會使用 get 訪問器以模擬活動綁定,以便在任意導出模塊中都可體現對單一模塊內變量的更改。此次變更的目標,在于進一步改善 TypeScript 輸出代碼與 ECMAScript 模塊的兼容度。
關于更多詳細信息,請參閱 這項變更的 pull 請求:
https://github.com/microsoft/TypeScript/pull/359670
導出結果的提升與初始賦值
配合 ES5 及更高版本中的 CommonJS 等目標模塊系統,TypeScript 現在能夠將導出的聲明提升至文件頂部。這一改變意味著 TypeScript 的導出結果與 ECMAScript 模塊將更加兼容。代碼示例如下:
export?*?from?"mod";export?const?nameFromMod =?0;
此前的輸出結果為:
__exportStar(exports, require("mod"));exports.nameFromMod =?0;
但由于導出結果現在使用 get- 訪問器,__exportStar 的存在使得賦值操作因該訪問器無法被賦值簡單覆蓋而失敗。在 TypeSCript 3.9 中,您需要使用以下命令:
exports.nameFromMod =?void?0;__exportStar(exports, require("mod"));exports.nameFromMod =?0;
若需了解詳細信息,請參閱原始 pull 請求:
https://github.com/microsoft/TypeScript/pull/37093
下一階段目標
我們希望 TypeScript 3.9 能進一步提升您的日常開發體驗并加快開發速度。關于后續版本,歡迎大家關注我們的 4.0 迭代計劃與功能發展路線圖。
4.0迭代計劃:https://github.com/microsoft/TypeScript/issues/38510
功能發展路線圖:https://github.com/Microsoft/TypeScript/wiki/Roadmap
延伸閱讀
https://devblogs.microsoft.com/typescript/announcing-typescript-3-9/