型別推斷與 Promise.all 的改進
近期版本的 TypeScript(大約 3.7 左右)更新了 Promise.all 和 Promise.race 等函式的宣告。遺憾的是,這引入了一些迴歸問題,尤其是在混合使用包含 null 或 undefined 的值時。
tsinterface 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,導致 lion 的型別被“汙染”幷包含了 undefined。
多虧了來自 Jack Bates 的 Pull Request,這一問題已在 TypeScript 3.9 中透過改進推斷流程得到修復。上述程式碼不再報錯。如果您因 Promise 相關問題而停留在舊版 TypeScript,我們鼓勵您嘗試一下 3.9!
關於 awaited 型別?
如果您一直在關注我們的問題跟蹤器和設計會議記錄,可能已經瞭解到關於 名為 awaited 的新型別運算子 的工作。該型別運算子的目標是準確模擬 JavaScript 中 Promise 的解包(unwrapping)方式。
我們最初預計在 TypeScript 3.9 中釋出 awaited,但在使用現有程式碼庫執行早期的 TypeScript 構建版本後,我們意識到該功能在順利推廣給所有人之前還需要更多的設計工作。因此,我們決定將該功能從主分支中撤出,直到我們更有把握為止。我們將繼續對該功能進行試驗,但不會將其作為本次釋出的一部分。
速度改進
TypeScript 3.9 帶來了多項速度改進。在觀察到使用 material-ui 和 styled-components 等包時出現極其緩慢的編輯/編譯速度後,我們的團隊一直專注於效能最佳化。我們進行了深入研究,透過一系列不同的 Pull Request,優化了涉及大型聯合型別、交叉型別、條件型別和對映型別的某些特殊情況。
- 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 Request 中的每一項都使特定程式碼庫的編譯時間減少了約 5-10%。總的來說,我們認為 material-ui 的編譯時間減少了約 40%!
我們還對編輯器場景中的檔案重新命名功能進行了一些更改。我們從 Visual Studio Code 團隊獲悉,在重新命名檔案時,僅確定需要更新哪些 import 語句可能就需要 5 到 10 秒。TypeScript 3.9 透過 更改編譯器和語言服務快取檔案查詢的內部機制 解決了這個問題。
雖然仍有改進空間,但我們希望這項工作能為每個人帶來更迅捷的體驗!
// @ts-expect-error 註釋
想象一下,我們正在用 TypeScript 編寫一個庫,並匯出了一個名為 doStuff 的函式作為公共 API 的一部分。該函式的型別宣告它接受兩個 string 引數,以便其他 TypeScript 使用者可以獲得型別檢查錯誤,但它也會執行執行時檢查(可能僅在開發版本中),以便給 JavaScript 使用者提供有用的錯誤提示。
tsfunction doStuff(abc: string, xyz: string) {assert(typeof abc === "string");assert(typeof xyz === "string");// do some stuff}
這樣,TypeScript 使用者在誤用該函式時會得到紅色的波浪線和錯誤訊息,而 JavaScript 使用者會得到一個斷言錯誤。我們希望測試這種行為,所以我們編寫了一個單元測試。
tsexpect(() => {doStuff(123, 456);}).toThrow();
遺憾的是,如果我們的測試是用 TypeScript 編寫的,TypeScript 會給我們報錯!
tsdoStuff(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// @ts-expect-errorconsole.log(47 * "octopus");
而以下程式碼
ts// @ts-expect-errorconsole.log(1 + 1);
會導致錯誤
Unused '@ts-expect-error' directive.
我們要衷心感謝實現此功能的貢獻者 Josh Goldberg。更多資訊請檢視 ts-expect-error 的 Pull Request。
ts-ignore 還是 ts-expect-error?
在某些方面,// @ts-expect-error 可以作為抑制註釋,類似於 // @ts-ignore。不同之處在於,如果下一行程式碼沒有錯誤,// @ts-ignore 不會執行任何操作。
您可能想將現有的 // @ts-ignore 註釋切換為 // @ts-expect-error,也可能在考慮未來程式碼中該選哪一個。雖然這完全取決於您和您的團隊,但我們對在特定情況下如何選擇有一些建議。
在以下情況下選擇 ts-expect-error:
- 您正在編寫測試程式碼,並且實際上希望型別系統在操作時報錯
- 您預期很快會有修復,並且只需要一個快速的臨時方案
- 您處於一個規模適中、團隊積極向上的專案中,希望在受影響的程式碼再次合法後儘快移除抑制註釋
在以下情況下選擇 ts-ignore:
- 您有一個較大的專案,並且在沒有明確所有者的程式碼中出現了新錯誤
- 您正在兩個不同版本的 TypeScript 之間進行升級,一行程式碼在一個版本中報錯但在另一個版本中不報錯
- 您實在沒時間決定這兩個選項中哪個更好
條件表示式中的未呼叫函式檢查
在 TypeScript 3.7 中,我們引入了未呼叫函式檢查,用於在您忘記呼叫函式時報告錯誤。
tsfunction 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 的 Pull Request,此功能現在也支援三元條件表示式(即 cond ? trueExpr : falseExpr 語法)。
tsdeclare 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);}}
https://github.com/microsoft/TypeScript/issues/36048
編輯器改進
TypeScript 編譯器不僅為大多數主要編輯器提供 TypeScript 編輯體驗,還為 Visual Studio 系列編輯器等提供 JavaScript 體驗。在編輯器中使用新的 TypeScript/JavaScript 功能的方式因編輯器而異,但:
- Visual Studio Code 支援 選擇不同版本的 TypeScript。此外,還有 JavaScript/TypeScript Nightly 擴充套件 來保持前沿體驗(通常非常穩定)。
- Visual Studio 2017/2019 擁有 [上述 SDK 安裝程式] 和 MSBuild 安裝。
- Sublime Text 3 支援 選擇不同版本的 TypeScript
JavaScript 中的 CommonJS 自動匯入
一個重大的改進在於使用 CommonJS 模組的 JavaScript 檔案中的自動匯入。
在舊版本中,TypeScript 總是假設無論您的檔案是什麼,您都想要 ECMAScript 風格的匯入,例如
jsimport * as fs from "fs";
然而,並非每個人在編寫 JavaScript 檔案時都以 ECMAScript 風格的模組為目標。許多使用者仍在使用 CommonJS 風格的 require(...) 匯入,如下所示
jsconst fs = require("fs");
TypeScript 現在會自動檢測您正在使用的匯入型別,以保持檔案風格的整潔和一致。
有關該更改的更多詳細資訊,請參見 相應的 Pull Request。
程式碼操作保留換行符
TypeScript 的重構和快速修復在保留換行符方面通常做得不夠好。作為一個非常基礎的例子,請看以下程式碼。
tsconst 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*/ 的範圍並提取到新函式中,我們將得到如下程式碼。
tsconst 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 3.9 做了更多工作來保留我們所寫的程式碼格式。
tsconst 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 Request 中檢視有關實現的更多資訊
缺少 return 表示式的快速修復
有時我們可能會忘記返回函式中最後一條語句的值,特別是在向箭頭函式新增大括號時。
ts// beforelet f1 = () => 42;// oops - not the same!let f2 = () => {42;};
多虧了來自社群成員 Wenlu Wang 的 Pull Request,TypeScript 可以提供快速修復來新增丟失的 return 語句、刪除大括號,或為看起來像物件字面量的箭頭函式體新增括號。

支援“解決方案風格”(Solution Style)的 tsconfig.json 檔案
編輯器需要確定一個檔案屬於哪個配置檔案,以便應用適當的選項並找出當前“專案”中還包含哪些檔案。預設情況下,由 TypeScript 語言伺服器驅動的編輯器透過向上遍歷每個父目錄來查詢 tsconfig.json 來做到這一點。
這種情況在 tsconfig.json 僅用於引用其他 tsconfig.json 檔案時會略微失效。
// tsconfig.json{"": [],"": [{ "path": "./tsconfig.shared.json" },{ "path": "./tsconfig.frontend.json" },{ "path": "./tsconfig.backend.json" }]}
這種只負責管理其他專案檔案的檔案在某些環境中通常被稱為“解決方案”(solution)。在這裡,這些 tsconfig.*.json 檔案都不會被伺服器拾取,但我們確實希望語言伺服器能理解當前的 .ts 檔案很可能屬於此根 tsconfig.json 中提到的專案之一。
TypeScript 3.9 為編輯場景增加了對該配置的支援。有關更多詳細資訊,請檢視 新增此功能的 Pull Request。
破壞性變更
可選鏈和非空斷言解析的差異
TypeScript 最近實現了可選鏈運算子,但我們收到使用者反饋稱,可選鏈 (?.) 與非空斷言運算子 (!) 的行為極其違反直覺。
具體來說,在之前的版本中,程式碼
tsfoo?.bar!.baz;
被解釋為等同於以下 JavaScript。
js(foo?.bar).baz;
在上述程式碼中,括號停止了可選鏈的“短路”行為,因此如果 foo 是 undefined,訪問 baz 將導致執行時錯誤。
指出此行為的 Babel 團隊以及大多數向我們提供反饋的使用者都認為這種行為是錯誤的。我們也這麼認為!我們聽到的最多的是 ! 運算子應該直接“消失”,因為我們的意圖是從 bar 的型別中移除 null 和 undefined。
換句話說,大多數人認為原始程式碼片段應該被解釋為
jsfoo?.bar.baz;
當 foo 為 undefined 時,它只會求值為 undefined。
這是一個重大更改,但我們相信大多數程式碼在編寫時都是考慮到這種新解釋的。希望恢復舊行為的使用者可以在 ! 運算子的左側新增明確的括號。
tsfoo?.bar!.baz;
} 和 > 現在是無效的 JSX 文字字元
JSX 規範禁止在文字位置使用 } 和 > 字元。TypeScript 和 Babel 都決定執行此規則以更符合規範。插入這些字元的新方法是使用 HTML 轉義程式碼(例如 <span> 2 > 1 </span>)或插入帶有字串字面量的表示式(例如 <span> 2 {">"} 1 </span>)。
幸運的是,多虧了來自 Brad Zacher 執行此規則的 Pull Request,您將收到如下類似的錯誤訊息
Unexpected token. Did you mean `{'>'}` or `>`?Unexpected token. Did you mean `{'}'}` or `}`?
例如:
tsxlet directions = <span>Navigate to: Menu Bar > Tools > Options</span>;// ~ ~// Unexpected token. Did you mean `{'>'}` or `>`?
該錯誤訊息帶有一個便捷的快速修復,並且多虧了 Alexander Tarasyuk,如果您有很多錯誤,您可以批次應用這些更改。
對交叉型別和可選屬性的更嚴格檢查
通常,如果 A 或 B 可分配給 C,則交叉型別 A & B 可分配給 C;然而,這有時在可選屬性上會有問題。例如,請看以下程式碼
tsinterface 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 中,只要交叉型別中的每個型別都是具體的物件型別,型別系統就會同時考慮所有屬性。因此,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 Request。
透過判別屬性縮減交叉型別
在某些情況下,您可能會得到描述根本不存在的值的型別。例如
tsdeclare 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 本身的型別為 never,因為 "circle" & "square" 描述了一組永不存在的值。
在 TypeScript 3.9 中,型別系統在此處更加激進——它注意到由於 kind 屬性的存在,無法交叉 Circle 和 Square。因此,它沒有將 z.kind 的型別摺疊為 never,而是將 z 本身 (Circle & Square) 的型別摺疊為 never。這意味著上面的程式碼現在會報錯
Property 'kind' does not exist on type 'never'.
我們觀察到的大多數破壞似乎都對應於稍微不正確的型別宣告。有關更多詳細資訊,請參閱原始 Pull Request。
Getter/Setter 不再可列舉
在舊版本的 TypeScript 中,類中的 get 和 set 訪問器以一種使它們可列舉的方式發出;然而,這不符合 ECMAScript 規範,該規範規定它們必須是不可列舉的。因此,目標為 ES5 和 ES2015 的 TypeScript 程式碼在行為上可能會有所不同。
多虧了來自 GitHub 使用者 pathurs 的 Pull Request,TypeScript 3.9 現在在這方面更符合 ECMAScript。
擴充套件 any 的型別引數不再表現為 any
在之前的 TypeScript 版本中,約束為 any 的型別引數可以被視為 any。
tsfunction foo<T extends any>(arg: T) {arg.spfjgerijghoied; // no error!}
這是一個疏忽,因此 TypeScript 3.9 採取了更保守的方法,並對這些可疑操作發出錯誤。
tsfunction foo<T extends any>(arg: T) {arg.spfjgerijghoied;// ~~~~~~~~~~~~~~~// Property 'spfjgerijghoied' does not exist on type 'T'.}
export * 總是被保留
在以前的 TypeScript 版本中,如果 foo 沒有匯出任何值,則像 export * from "foo" 這樣的宣告會在我們的 JavaScript 輸出中被刪除。這種發出方式是有問題的,因為它是型別驅動的,無法由 Babel 模擬。TypeScript 3.9 將始終發出這些 export * 宣告。在實踐中,我們預計這不會破壞太多現有程式碼。
更多 libdom.d.ts 最佳化
我們正在繼續將更多 TypeScript 的內建 .d.ts 庫 (lib.d.ts 及相關係列) 移至從 DOM 規範直接生成的 Web IDL 檔案。因此,刪除了一些與媒體訪問相關的特定廠商型別。
將此檔案新增到您專案的環境 *.d.ts 中即可恢復它們
tsinterface AudioTrackList {[Symbol.iterator](): IterableIterator<AudioTrack>;}interface HTMLVideoElement {readonly audioTracks: AudioTrackListmsFrameStep(forward: boolean): void;msInsertVideoEffect(activatableClassId: string, effectRequired: boolean, config?: any): void;msSetVideoRectangle(left: number, top: number, right: number, bottom: number): void;webkitEnterFullScreen(): void;webkitEnterFullscreen(): void;webkitExitFullScreen(): void;webkitExitFullscreen(): void;msHorizontalMirror: boolean;readonly msIsLayoutOptimalForPlayback: boolean;readonly msIsStereo3D: boolean;msStereo3DPackingMode: string;msStereo3DRenderMode: string;msZoom: boolean;onMSVideoFormatChanged: ((this: HTMLVideoElement, ev: Event) => any) | null;onMSVideoFrameStepCompleted: ((this: HTMLVideoElement, ev: Event) => any) | null;onMSVideoOptimalLayoutChanged: ((this: HTMLVideoElement, ev: Event) => any) | null;webkitDisplayingFullscreen: boolean;webkitSupportsFullscreen: boolean;}interface MediaError {readonly msExtendedCode: number;readonly MS_MEDIA_ERR_ENCRYPTED: number;}