TypeScript 4.4

別名條件和判別式的控制流分析

在 JavaScript 中,我們經常需要以不同的方式探查一個值,並根據其型別的進一步資訊執行不同的操作。TypeScript 理解這些檢查,並將它們稱為型別保護(type guards)。型別檢查器利用所謂的控制流分析(control flow analysis)來檢視我們在給定程式碼段之前是否使用了型別保護,而不是要求我們在每次使用變數時都向 TypeScript 證明其型別。

例如,我們可以編寫如下程式碼:

ts
function foo(arg: unknown) {
if (typeof arg === "string") {
console.log(arg.toUpperCase());
(parameter) arg: string
}
}
Try

在此示例中,我們檢查了 arg 是否為 string。TypeScript 識別出 typeof arg === "string" 這個檢查,將其視為一個型別保護,並知道在 if 塊內 arg 是一個 string。這讓我們能夠訪問 string 的方法(如 toUpperCase())而不會報錯。

但是,如果我們把條件移動到一個名為 argIsString 的常量中會發生什麼呢?

ts
// In TS 4.3 and below
function foo(arg: unknown) {
const argIsString = typeof arg === "string";
if (argIsString) {
console.log(arg.toUpperCase());
// ~~~~~~~~~~~
// Error! Property 'toUpperCase' does not exist on type 'unknown'.
}
}

在之前的 TypeScript 版本中,這會產生錯誤——儘管 argIsString 被賦值為一個型別保護,但 TypeScript 丟失了該資訊。這很遺憾,因為我們可能希望在多個地方重複使用同一個檢查。為了解決這個問題,使用者往往不得不重複程式碼或使用型別斷言(即型別轉換)。

在 TypeScript 4.4 中,情況不再如此。上述示例可以正常工作且沒有錯誤!當 TypeScript 看到我們在測試一個常量值時,它會進行額外的工作,檢視該常量是否包含型別保護。如果該型別保護作用於 constreadonly 屬性或未修改的引數,那麼 TypeScript 就能夠適當地收窄(narrow)該值。

不僅是 typeof 檢查,不同型別的型別保護條件都能被保留。例如,對判別式聯合型別(discriminated unions)的檢查也能完美執行。

ts
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; sideLength: number };
 
function area(shape: Shape): number {
const isCircle = shape.kind === "circle";
if (isCircle) {
// We know we have a circle here!
return Math.PI * shape.radius ** 2;
} else {
// We know we're left with a square here!
return shape.sideLength ** 2;
}
}
Try

4.4 中對判別式的分析也更深入了——我們現在可以將判別式提取出來,TypeScript 依然能夠對原始物件進行收窄。

ts
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; sideLength: number };
 
function area(shape: Shape): number {
// Extract out the 'kind' field first.
const { kind } = shape;
 
if (kind === "circle") {
// We know we have a circle here!
return Math.PI * shape.radius ** 2;
} else {
// We know we're left with a square here!
return shape.sideLength ** 2;
}
}
Try

作為另一個例子,這是一個檢查其兩個輸入是否包含內容的方法:

ts
function doSomeChecks(
inputA: string | undefined,
inputB: string | undefined,
shouldDoExtraWork: boolean
) {
const mustDoWork = inputA && inputB && shouldDoExtraWork;
if (mustDoWork) {
// We can access 'string' properties on both 'inputA' and 'inputB'!
const upperA = inputA.toUpperCase();
const upperB = inputB.toUpperCase();
// ...
}
}
Try

如果 mustDoWorktrue,TypeScript 可以理解 inputAinputB 同時存在。這意味著我們不必編寫非空斷言(如 inputA!)來向 TypeScript 證明 inputA 不是 undefined

這裡一個很棒的功能是,這種分析是傳遞性的。TypeScript 會透過常量進行跳轉,以理解你已經執行了哪些檢查。

ts
function f(x: string | number | boolean) {
const isString = typeof x === "string";
const isNumber = typeof x === "number";
const isStringOrNumber = isString || isNumber;
if (isStringOrNumber) {
x;
(parameter) x: string | number
} else {
x;
(parameter) x: boolean
}
}
Try

注意,這裡有一個截止點——TypeScript 在檢查這些條件時不會進行無限深的巢狀,但其分析深度對於大多數檢查來說已經足夠了。

這個功能應該能讓許多直觀的 JavaScript 程式碼在 TypeScript 中“直接執行”,而不會產生阻礙。更多詳細資訊,請檢視 GitHub 上的實現

Symbol 和模板字串模式索引簽名

TypeScript 允許我們使用索引簽名(index signatures)來描述物件,其中每個屬性都必須具有特定的型別。這允許我們將這些物件用作字典類型別,我們可以使用字串鍵透過方括號對其進行索引。

例如,我們可以編寫一個帶有索引簽名的型別,它接收 string 鍵並對映到 boolean 值。如果我們嘗試分配除 boolean 以外的任何值,就會收到錯誤。

ts
interface BooleanDictionary {
[key: string]: boolean;
}
 
declare let myDict: BooleanDictionary;
 
// Valid to assign boolean values
myDict["foo"] = true;
myDict["bar"] = false;
 
// Error, "oops" isn't a boolean
myDict["baz"] = "oops";
Type 'string' is not assignable to type 'boolean'.2322Type 'string' is not assignable to type 'boolean'.
Try

雖然 Map 在這裡可能是一個更好的資料結構(特別是 Map<string, boolean>),但 JavaScript 物件通常更易於使用,或者恰好就是我們需要處理的資料形式。

同樣地,Array<T> 已經定義了一個 number 索引簽名,允許我們插入/檢索型別為 T 的值。

ts
// @errors: 2322 2375
// This is part of TypeScript's definition of the built-in Array type.
interface Array<T> {
[index: number]: T;
// ...
}
let arr = new Array<string>();
// Valid
arr[0] = "hello!";
// Error, expecting a 'string' value here
arr[1] = 123;

索引簽名對於表達大量的實際程式碼非常有用;然而,到目前為止,它們僅限於 stringnumber 鍵(並且 string 索引簽名有一個特意的特性,即它們可以接受 number 鍵,因為數字會被自動轉換為字串)。這意味著 TypeScript 不允許使用 symbol 鍵對物件進行索引。TypeScript 也無法對 string 鍵的某個子集建立索引簽名——例如,描述名稱以 data- 開頭的屬性的索引簽名。

TypeScript 4.4 解決了這些限制,並允許針對 symbol 和模板字串模式使用索引簽名。

例如,TypeScript 現在允許我們宣告一個可以使用任意 symbol 作為鍵的型別。

ts
interface Colors {
[sym: symbol]: number;
}
 
const red = Symbol("red");
const green = Symbol("green");
const blue = Symbol("blue");
 
let colors: Colors = {};
 
// Assignment of a number is allowed
colors[red] = 255;
let redVal = colors[red];
let redVal: number
 
colors[blue] = "da ba dee";
Type 'string' is not assignable to type 'number'.2322Type 'string' is not assignable to type 'number'.
Try

類似地,我們可以編寫帶有模板字串模式型別的索引簽名。這的一種用途是可以將以 data- 開頭的屬性從 TypeScript 的多餘屬性檢查(excess property checking)中排除。當我們向具有預期型別的物件字面量賦值時,TypeScript 會查詢預期型別中未宣告的多餘屬性。

ts
// @errors: 2322 2375
interface Options {
width?: number;
height?: number;
}
let a: Options = {
width: 100,
height: 100,
"data-blah": true,
};
interface OptionsWithDataProps extends Options {
// Permit any property starting with 'data-'.
[optName: `data-${string}`]: unknown;
}
let b: OptionsWithDataProps = {
width: 100,
height: 100,
"data-blah": true,
// Fails for a property which is not known, nor
// starts with 'data-'
"unknown-property": true,
};

關於索引簽名的最後一點說明是,它們現在允許聯合型別,只要它們是無限域原始型別的聯合——具體來說:

  • string
  • number
  • symbol
  • 模板字串模式(例如 `hello-${string}`

引數為這些型別聯合的索引簽名將被解構為多個不同的索引簽名。

ts
interface Data {
[optName: string | symbol]: any;
}
// Equivalent to
interface Data {
[optName: string]: any;
[optName: symbol]: any;
}

有關詳細資訊,請閱讀相關 PR

Catch 變數預設使用 unknown 型別 (--useUnknownInCatchVariables)

在 JavaScript 中,任何型別的值都可以透過 throw 丟擲並在 catch 子句中被捕獲。因此,TypeScript 歷史上將 catch 子句變數的型別設為 any,並且不允許任何其他型別註解。

ts
try {
// Who knows what this might throw...
executeSomeThirdPartyCode();
} catch (err) {
// err: any
console.error(err.message); // Allowed, because 'any'
err.thisWillProbablyFail(); // Allowed, because 'any' :(
}

自從 TypeScript 添加了 unknown 型別後,很明顯 unknownany 對於 catch 子句變數是更好的選擇(對於追求最高程度正確性和型別安全性的使用者而言),因為它更容易收窄,並強制我們對任意值進行測試。最終,TypeScript 4.0 允許使用者在每個 catch 子句變數上顯式指定 unknown(或 any)型別註解,以便我們可以按需選擇更嚴格的型別;然而,對於某些人來說,在每個 catch 子句上手動指定 : unknown 是一件繁瑣的工作。

這就是為什麼 TypeScript 4.4 引入了一個名為 useUnknownInCatchVariables 的新標誌。該標誌將 catch 子句變數的預設型別從 any 更改為 unknown

ts
try {
executeSomeThirdPartyCode();
} catch (err) {
// err: unknown
 
// Error! Property 'message' does not exist on type 'unknown'.
console.error(err.message);
'err' is of type 'unknown'.18046'err' is of type 'unknown'.
 
// Works! We can narrow 'err' from 'unknown' to 'Error'.
if (err instanceof Error) {
console.error(err.message);
}
}
Try

此標誌在 strict 選項系列下啟用。這意味著如果你使用 strict 檢查程式碼,該選項將自動開啟。你在 TypeScript 4.4 中可能會遇到如下錯誤:

Property 'message' does not exist on type 'unknown'.
Property 'name' does not exist on type 'unknown'.
Property 'stack' does not exist on type 'unknown'.

在不希望處理 catch 子句中的 unknown 變數的情況下,我們始終可以新增顯式的 : any 註解來放棄更嚴格的型別。

ts
try {
executeSomeThirdPartyCode();
} catch (err: any) {
console.error(err.message); // Works again!
}
Try

有關更多資訊,請檢視相關實現 PR

精確可選屬性型別 (--exactOptionalPropertyTypes)

在 JavaScript 中,讀取物件上缺失的屬性會產生 undefined 值。同時,也可能確實存在一個值為 undefined 的屬性。許多 JavaScript 程式碼傾向於將這兩種情況同等對待,因此最初 TypeScript 只是將每個可選屬性解釋為使用者在型別中編寫了 undefined。例如:

ts
interface Person {
name: string;
age?: number;
}

被視為等同於:

ts
interface Person {
name: string;
age?: number | undefined;
}

這意味著使用者可以顯式地將 undefined 寫入 age 的位置。

ts
const p: Person = {
name: "Daniel",
age: undefined, // This is okay by default.
};

因此,預設情況下,TypeScript 不區分屬性存在但值為 undefined 和屬性完全缺失這兩種情況。雖然這在大多數情況下有效,但並非所有的 JavaScript 程式碼都做相同的假設。像 Object.assignObject.keys、物件展開({ ...obj })和 for-in 迴圈等函式和運算子,會根據屬性是否真正存在於物件上而表現不同。在我們的 Person 示例中,如果 age 屬性在對其存在性敏感的上下文中被觀察,這可能會導致執行時錯誤。

在 TypeScript 4.4 中,新標誌 exactOptionalPropertyTypes 指定可選屬性型別應完全按書寫方式解釋,這意味著 | undefined 不會自動新增到該型別中。

ts
// With 'exactOptionalPropertyTypes' on:
const p: Person = {
Type '{ name: string; age: undefined; }' is not assignable to type 'Person' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties. Types of property 'age' are incompatible. Type 'undefined' is not assignable to type 'number'.2375Type '{ name: string; age: undefined; }' is not assignable to type 'Person' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties. Types of property 'age' are incompatible. Type 'undefined' is not assignable to type 'number'.
name: "Daniel",
age: undefined, // Error! undefined isn't a number
};
Try

此標誌不是 strict 系列的一部分,如果您希望此行為,需要顯式開啟。它還要求同時啟用 strictNullChecks。我們一直在對 DefinitelyTyped 和其他定義進行更新,以儘可能使過渡直接,但根據您的程式碼結構,您可能會遇到一些阻力。

有關更多資訊,您可以檢視此處的實現 PR

類中的 static

TypeScript 4.4 帶來了對類中 static的支援,這是一項即將推出的 ECMAScript 功能,可以幫助您為靜態成員編寫更復雜的初始化程式碼。

ts
class Foo {
static count = 0;
 
// This is a static block:
static {
if (someCondition()) {
Foo.count++;
}
}
}
Try

這些靜態塊允許您編寫一系列具有自身作用域的語句,這些語句可以訪問包含類中的私有欄位。這意味著我們可以編寫具有語句編寫所有功能、不會洩露變數且能完全訪問類內部的初始化程式碼。

ts
class Foo {
static #count = 0;
 
get count() {
return Foo.#count;
}
 
static {
try {
const lastInstances = loadLastInstances();
Foo.#count += lastInstances.length;
}
catch {}
}
}
Try

如果沒有 static 塊,編寫上述程式碼是可能的,但通常涉及幾種不同型別的駭客手段,必須在某些方面做出妥協。

請注意,一個類可以有多個 static 塊,它們按照書寫的順序執行。

ts
// Prints:
// 1
// 2
// 3
class Foo {
static prop = 1
static {
console.log(Foo.prop++);
}
static {
console.log(Foo.prop++);
}
static {
console.log(Foo.prop++);
}
}
Try

我們要感謝 Wenlu Wang 為 TypeScript 實現了此功能。更多詳細資訊,您可以檢視該 PR

tsc --help 更新與改進

TypeScript 的 --help 選項煥然一新!感謝 Song Gao 的部分貢獻,我們對編譯器選項的描述進行了更新,並以顏色和其他視覺分隔重新設計了 --help 選單

The new TypeScript --help menu where the output is bucketed into several different areas

您可以在原始提案討論帖中閱讀更多內容。

效能改進

更快的宣告檔案生成

TypeScript 現在會快取內部符號在不同上下文中的可訪問性,以及特定型別的列印方式。這些更改可以提升 TypeScript 在處理具有相當複雜型別的程式碼時的總體效能,在使用 declaration 標誌生成 .d.ts 檔案時尤其明顯。

在此處檢視更多詳細資訊。.

更快的路徑規範化

TypeScript 通常需要對檔案路徑執行多種“規範化”以使其成為編譯器在各處都能使用的統一格式。這涉及諸如將反斜槓替換為斜槓,或移除路徑中中間的 /.//../ 段等操作。當 TypeScript 需要處理數百萬個此類路徑時,這些操作會變得有點緩慢。在 TypeScript 4.4 中,路徑首先會經過快速檢查,以檢視是否根本不需要規範化。這些改進共同使大型專案的載入時間縮短了 5-10%,在我們內部測試的超大規模專案中,提升幅度更是顯著。

有關更多詳細資訊,您可以檢視路徑段規範化的 PR 以及斜槓規範化的 PR

更快的路徑對映

TypeScript 現在快取其構建路徑對映(使用 tsconfig.json 中的 paths 選項)的方式。對於具有幾百個對映的專案,減少量是顯著的。您可以在變更本身中看到更多資訊。

更快的 --strict 增量構建

作為一個實際上是 bug 的問題,TypeScript 會在開啟 strict 的情況下,在 incremental 編譯期間重複進行型別檢查工作。這導致許多構建的速度與關閉 incremental 時一樣慢。TypeScript 4.4 修復了這個問題,該更改也已向後移植到 TypeScript 4.3。

檢視更多此處

針對大型輸出更快的原始碼對映生成

TypeScript 4.4 為超大輸出檔案的原始碼對映生成添加了一項最佳化。在構建舊版本的 TypeScript 編譯器時,這使得發射(emit)時間減少了約 8%。

我們要感謝 David Michon,他提供了一個簡單且乾淨的更改來實現這一效能提升。

更快的 --force 構建

當對專案引用使用 --build 模式時,TypeScript 必須執行最新性檢查以確定哪些檔案需要重新構建。然而,執行 --force 構建時,這些資訊是無關緊要的,因為每個專案依賴都將從頭開始重新構建。在 TypeScript 4.4 中,--force 構建避免了這些不必要的步驟並開始完全構建。檢視更多關於該更改的內容此處

JavaScript 的拼寫建議

TypeScript 支援 Visual Studio 和 Visual Studio Code 等編輯器中的 JavaScript 編輯體驗。大多數時候,TypeScript 會盡量不去打擾 JavaScript 檔案;然而,TypeScript 通常擁有大量資訊來進行自信的建議,並以不那麼“侵入式”的方式呈現建議。

這就是為什麼 TypeScript 現在在純 JavaScript 檔案中釋出拼寫建議——那些沒有 // @ts-check 或專案中關閉了 checkJs 的檔案。這些與 TypeScript 檔案中已有的“你的意思是……”建議相同,現在它們以某種形式提供給所有 JavaScript 檔案。

這些拼寫建議可以提供一個細微的線索,表明您的程式碼可能有錯。我們在測試此功能時,在現有程式碼中發現了一些 bug!

有關此新功能的更多詳細資訊,請檢視 PR

內嵌提示 (Inlay Hints)

TypeScript 4.4 提供了對內嵌提示的支援,這有助於在程式碼中顯示有用的資訊,如引數名稱和返回型別。您可以將其視為一種友好的“幽靈文字”。

A preview of inlay hints in Visual Studio Code

此功能由 Wenlu Wang 構建,其PR 包含更多詳細資訊。

Wenlu 還貢獻了Visual Studio Code 中內嵌提示的整合,該整合已作為2021 年 7 月 (1.59) 版本的一部分釋出。如果您想嘗試內嵌提示,請確保您使用的是編輯器的最新穩定版預覽版 (insiders)。您還可以在 Visual Studio Code 的設定中修改何時何地顯示內嵌提示。

自動匯入在補全列表中顯示真實路徑

當像 Visual Studio Code 這樣的編輯器顯示補全列表時,包含自動匯入的補全會顯示給定模組的路徑;然而,此路徑通常不是 TypeScript 最終放入模組識別符號(module specifier)中的內容。該路徑通常是相對於工作區的,這意味著如果您從像 moment 這樣的包中匯入,您通常會看到類似 node_modules/moment 的路徑。

A completion list containing unwieldy paths containing 'node_modules'. For example, the label for 'calendarFormat' is 'node_modules/moment/moment' instead of 'moment'.

這些路徑最終顯得笨拙且往往具有誤導性,特別是考慮到實際插入到您檔案中的路徑需要考慮 Node 的 node_modules 解析、路徑對映、符號連結和重新匯出。

這就是為什麼在 TypeScript 4.4 中,補全項標籤現在顯示將用於匯入的實際模組路徑!

A completion list containing clean paths with no intermediate 'node_modules'. For example, the label for 'calendarFormat' is 'moment' instead of 'node_modules/moment/moment'.

由於此計算可能開銷較大,包含許多自動匯入的補全列表可能會在您鍵入更多字元時分批填充最終的模組識別符號。您可能有時仍會看到舊的工作區相對路徑標籤;然而,隨著您的編輯體驗“預熱”,它們應該會在再敲擊一兩個鍵後替換為實際路徑。

破壞性變更

TypeScript 4.4 的 lib.d.ts 更改

與每個 TypeScript 版本一樣,lib.d.ts 的宣告(特別是為 Web 上下文生成的宣告)發生了變化。您可以查閱我們已知的 lib.dom.d.ts 更改列表以瞭解哪些受到了影響。

針對匯入函式的更合規的間接呼叫

在早期版本的 TypeScript 中,呼叫 CommonJS、AMD 和其他非 ES 模組系統的匯入會設定所呼叫函式的 this 值。具體來說,在以下示例中,呼叫 fooModule.foo() 時,foo() 方法會將 fooModule 設定為 this 的值。

ts
// Imagine this is our imported module, and it has an export named 'foo'.
let fooModule = {
foo() {
console.log(this);
},
};
fooModule.foo();

這並不是 ECMAScript 中匯出函式在我們呼叫它們時應有的工作方式。這就是為什麼 TypeScript 4.4 透過使用以下發射方式,有意在呼叫匯入函式時丟棄 this 值。

ts
// Imagine this is our imported module, and it has an export named 'foo'.
let fooModule = {
foo() {
console.log(this);
},
};
// Notice we're actually calling '(0, fooModule.foo)' now, which is subtly different.
(0, fooModule.foo)();

您可以在此處閱讀更多關於這些更改的資訊

在 Catch 變數中使用 unknown

使用 strict 標誌執行的使用者可能會看到關於 catch 變數為 unknown 的新錯誤,特別是如果現有程式碼假設只有 Error 值被捕獲。這通常會導致如下錯誤訊息:

Property 'message' does not exist on type 'unknown'.
Property 'name' does not exist on type 'unknown'.
Property 'stack' does not exist on type 'unknown'.

為了繞過這個問題,您可以專門新增執行時檢查以確保丟擲的型別與您預期的型別匹配。否則,您可以使用型別斷言,為您的 catch 變數新增顯式的 : any,或者關閉 useUnknownInCatchVariables

更廣泛的 Always-Truthy Promise 檢查

在之前的版本中,TypeScript 引入了“Always Truthy Promise 檢查”來捕捉可能忘記了 await 的程式碼;然而,這些檢查僅適用於命名宣告。這意味著雖然這段程式碼會正確收到錯誤……

ts
async function foo(): Promise<boolean> {
return false;
}
async function bar(): Promise<string> {
const fooResult = foo();
if (fooResult) {
// <- error! :D
return "true";
}
return "false";
}

……但以下程式碼則不會。

ts
async function foo(): Promise<boolean> {
return false;
}
async function bar(): Promise<string> {
if (foo()) {
// <- no error :(
return "true";
}
return "false";
}

TypeScript 4.4 現在會將兩者都標記出來。有關更多資訊,請閱讀原始更改資訊

抽象屬性不允許使用初始化器

以下程式碼現在會產生錯誤,因為抽象屬性不能有初始化器:

ts
abstract class C {
abstract prop = 1;
// ~~~~
// Property 'prop' cannot have an initializer because it is marked abstract.
}

相反,您只能為該屬性指定一個型別:

ts
abstract class C {
abstract prop: number;
}

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

此頁面的貢獻者
OTOrta Therox (2)
ELEliran Levi (1)
ABAndrew Branch (1)
JBJack Bates (1)

最後更新:2026 年 3 月 27 日