TypeScript 3.9

型別推斷與 Promise.all 的改進

近期版本的 TypeScript(大約 3.7 左右)更新了 Promise.allPromise.race 等函式的宣告。遺憾的是,這引入了一些迴歸問題,尤其是在混合使用包含 nullundefined 的值時。

ts
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,導致 lion 的型別被“汙染”幷包含了 undefined

多虧了來自 Jack BatesPull 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,優化了涉及大型聯合型別、交叉型別、條件型別和對映型別的某些特殊情況。

這些 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 使用者提供有用的錯誤提示。

ts
function doStuff(abc: string, xyz: string) {
assert(typeof abc === "string");
assert(typeof xyz === "string");
// do some stuff
}

這樣,TypeScript 使用者在誤用該函式時會得到紅色的波浪線和錯誤訊息,而 JavaScript 使用者會得到一個斷言錯誤。我們希望測試這種行為,所以我們編寫了一個單元測試。

ts
expect(() => {
doStuff(123, 456);
}).toThrow();

遺憾的是,如果我們的測試是用 TypeScript 編寫的,TypeScript 會給我們報錯!

ts
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
// @ts-expect-error
console.log(47 * "octopus");

而以下程式碼

ts
// @ts-expect-error
console.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 中,我們引入了未呼叫函式檢查,用於在您忘記呼叫函式時報告錯誤。

ts
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 TarasyukPull Request,此功能現在也支援三元條件表示式(即 cond ? trueExpr : falseExpr 語法)。

ts
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);
}
}

https://github.com/microsoft/TypeScript/issues/36048

編輯器改進

TypeScript 編譯器不僅為大多數主要編輯器提供 TypeScript 編輯體驗,還為 Visual Studio 系列編輯器等提供 JavaScript 體驗。在編輯器中使用新的 TypeScript/JavaScript 功能的方式因編輯器而異,但:

JavaScript 中的 CommonJS 自動匯入

一個重大的改進在於使用 CommonJS 模組的 JavaScript 檔案中的自動匯入。

在舊版本中,TypeScript 總是假設無論您的檔案是什麼,您都想要 ECMAScript 風格的匯入,例如

js
import * as fs from "fs";

然而,並非每個人在編寫 JavaScript 檔案時都以 ECMAScript 風格的模組為目標。許多使用者仍在使用 CommonJS 風格的 require(...) 匯入,如下所示

js
const fs = require("fs");

TypeScript 現在會自動檢測您正在使用的匯入型別,以保持檔案風格的整潔和一致。

有關該更改的更多詳細資訊,請參見 相應的 Pull Request

程式碼操作保留換行符

TypeScript 的重構和快速修復在保留換行符方面通常做得不夠好。作為一個非常基礎的例子,請看以下程式碼。

ts
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*/ 的範圍並提取到新函式中,我們將得到如下程式碼。

ts
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);
}
}

Extracting the for loop to a function in older versions of TypeScript. A newline is not preserved.

這並不理想——我們的 for 迴圈中每條語句之間有一個空行,但重構將其去掉了!TypeScript 3.9 做了更多工作來保留我們所寫的程式碼格式。

ts
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);
}
}

Extracting the for loop to a function in TypeScript 3.9. A newline is preserved.

您可以在 此 Pull Request 中檢視有關實現的更多資訊

缺少 return 表示式的快速修復

有時我們可能會忘記返回函式中最後一條語句的值,特別是在向箭頭函式新增大括號時。

ts
// before
let f1 = () => 42;
// oops - not the same!
let f2 = () => {
42;
};

多虧了來自社群成員 Wenlu WangPull Request,TypeScript 可以提供快速修復來新增丟失的 return 語句、刪除大括號,或為看起來像物件字面量的箭頭函式體新增括號。

TypeScript fixing an error where no expression is returned by adding a return statement or removing curly braces.

支援“解決方案風格”(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 最近實現了可選鏈運算子,但我們收到使用者反饋稱,可選鏈 (?.) 與非空斷言運算子 (!) 的行為極其違反直覺。

具體來說,在之前的版本中,程式碼

ts
foo?.bar!.baz;

被解釋為等同於以下 JavaScript。

js
(foo?.bar).baz;

在上述程式碼中,括號停止了可選鏈的“短路”行為,因此如果 fooundefined,訪問 baz 將導致執行時錯誤。

指出此行為的 Babel 團隊以及大多數向我們提供反饋的使用者都認為這種行為是錯誤的。我們也這麼認為!我們聽到的最多的是 ! 運算子應該直接“消失”,因為我們的意圖是從 bar 的型別中移除 nullundefined

換句話說,大多數人認為原始程式碼片段應該被解釋為

js
foo?.bar.baz;

fooundefined 時,它只會求值為 undefined

這是一個重大更改,但我們相信大多數程式碼在編寫時都是考慮到這種新解釋的。希望恢復舊行為的使用者可以在 ! 運算子的左側新增明確的括號。

ts
foo?.bar!.baz;

}> 現在是無效的 JSX 文字字元

JSX 規範禁止在文字位置使用 }> 字元。TypeScript 和 Babel 都決定執行此規則以更符合規範。插入這些字元的新方法是使用 HTML 轉義程式碼(例如 <span> 2 &gt 1 </span>)或插入帶有字串字面量的表示式(例如 <span> 2 {">"} 1 </span>)。

幸運的是,多虧了來自 Brad Zacher 執行此規則的 Pull Request,您將收到如下類似的錯誤訊息

Unexpected token. Did you mean `{'>'}` or `>`?
Unexpected token. Did you mean `{'}'}` or `}`?

例如:

tsx
let directions = <span>Navigate to: Menu Bar > Tools > Options</span>;
// ~ ~
// Unexpected token. Did you mean `{'>'}` or `>`?

該錯誤訊息帶有一個便捷的快速修復,並且多虧了 Alexander Tarasyuk,如果您有很多錯誤,您可以批次應用這些更改

對交叉型別和可選屬性的更嚴格檢查

通常,如果 AB 可分配給 C,則交叉型別 A & B 可分配給 C;然而,這有時在可選屬性上會有問題。例如,請看以下程式碼

ts
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 版本中,這是被允許的,因為雖然 AC 完全不相容,但 B 確實C 相容。

在 TypeScript 3.9 中,只要交叉型別中的每個型別都是具體的物件型別,型別系統就會同時考慮所有屬性。因此,TypeScript 會發現 A & Ba 屬性與 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

透過判別屬性縮減交叉型別

在某些情況下,您可能會得到描述根本不存在的值的型別。例如

ts
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);

這段程式碼有點奇怪,因為確實沒有辦法建立 CircleSquare 的交集——它們有兩個不相容的 kind 欄位。在之前的 TypeScript 版本中,此程式碼被允許,並且 kind 本身的型別為 never,因為 "circle" & "square" 描述了一組永不存在的值。

在 TypeScript 3.9 中,型別系統在此處更加激進——它注意到由於 kind 屬性的存在,無法交叉 CircleSquare。因此,它沒有將 z.kind 的型別摺疊為 never,而是將 z 本身 (Circle & Square) 的型別摺疊為 never。這意味著上面的程式碼現在會報錯

Property 'kind' does not exist on type 'never'.

我們觀察到的大多數破壞似乎都對應於稍微不正確的型別宣告。有關更多詳細資訊,請參閱原始 Pull Request

Getter/Setter 不再可列舉

在舊版本的 TypeScript 中,類中的 getset 訪問器以一種使它們可列舉的方式發出;然而,這不符合 ECMAScript 規範,該規範規定它們必須是不可列舉的。因此,目標為 ES5 和 ES2015 的 TypeScript 程式碼在行為上可能會有所不同。

多虧了來自 GitHub 使用者 pathursPull Request,TypeScript 3.9 現在在這方面更符合 ECMAScript。

擴充套件 any 的型別引數不再表現為 any

在之前的 TypeScript 版本中,約束為 any 的型別引數可以被視為 any

ts
function foo<T extends any>(arg: T) {
arg.spfjgerijghoied; // no error!
}

這是一個疏忽,因此 TypeScript 3.9 採取了更保守的方法,並對這些可疑操作發出錯誤。

ts
function 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 中即可恢復它們

ts
interface AudioTrackList {
[Symbol.iterator](): IterableIterator<AudioTrack>;
}
interface HTMLVideoElement {
readonly audioTracks: AudioTrackList
msFrameStep(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;
}

TypeScript 文件是一個開源專案。請傳送 Pull Request 幫助我們改進這些頁面 ❤

此頁面的貢獻者
OTOrta Therox (12)
SLShammel Lee (1)
NSNick Schonning (1)
MUMasato Urai (1)
HKHomyee King (1)
2+

最後更新:2026 年 3 月 27 日