TypeScript 4.9

satisfies 運算子

TypeScript 開發者經常面臨一個困境:我們既想確保某個表示式符合某種型別,又想保留該表示式的最具體型別以用於型別推斷。

例如:

ts
// Each property can be a string or an RGB tuple.
const palette = {
red: [255, 0, 0],
green: "#00ff00",
bleu: [0, 0, 255]
// ^^^^ sacrebleu - we've made a typo!
};
// We want to be able to use string methods on 'green'...
const greenNormalized = palette.green.toUpperCase();

請注意,我們寫成了 bleu,但可能本應寫成 blue。我們可以嘗試透過對 palette 使用型別註解來捕捉 bleu 這個拼寫錯誤,但這樣我們會丟失每個屬性的具體資訊。

ts
type Colors = "red" | "green" | "blue";
type RGB = [red: number, green: number, blue: number];
const palette: Record<Colors, string | RGB> = {
red: [255, 0, 0],
green: "#00ff00",
bleu: [0, 0, 255]
// ~~~~ The typo is now correctly detected
};
// But we now have an undesirable error here - 'palette.green' "could" be of type RGB and
// property 'toUpperCase' does not exist on type 'string | RGB'.
const greenNormalized = palette.green.toUpperCase();

新的 satisfies 運算子允許我們驗證表示式的型別是否符合某種型別,而不會更改該表示式的結果型別。例如,我們可以使用 satisfies 來驗證 palette 的所有屬性是否與 string | number[] 相容。

ts
type Colors = "red" | "green" | "blue";
type RGB = [red: number, green: number, blue: number];
const palette = {
red: [255, 0, 0],
green: "#00ff00",
bleu: [0, 0, 255]
// ~~~~ The typo is now caught!
} satisfies Record<Colors, string | RGB>;
// toUpperCase() method is still accessible!
const greenNormalized = palette.green.toUpperCase();

satisfies 可用於捕獲許多潛在錯誤。例如,我們可以確保物件擁有某種型別的所有鍵,且沒有多餘的鍵。

ts
type Colors = "red" | "green" | "blue";
// Ensure that we have exactly the keys from 'Colors'.
const favoriteColors = {
"red": "yes",
"green": false,
"blue": "kinda",
"platypus": false
// ~~~~~~~~~~ error - "platypus" was never listed in 'Colors'.
} satisfies Record<Colors, unknown>;
// All the information about the 'red', 'green', and 'blue' properties are retained.
const g: boolean = favoriteColors.green;

也許我們不在意屬性名是否完全匹配,但我們在意每個屬性的型別。在這種情況下,我們也可以確保物件的所有屬性值都符合某種型別。

ts
type RGB = [red: number, green: number, blue: number];
const palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0]
// ~~~~~~ error!
} satisfies Record<string, string | RGB>;
// Information about each property is still maintained.
const redComponent = palette.red.at(0);
const greenNormalized = palette.green.toUpperCase();

更多示例,請參閱提議此功能的 issue實現該功能的 pull request。我們衷心感謝 Oleksandr Tarasiuk,他與我們一起實現了這一功能並進行了迭代。

使用 in 運算子進行未列出屬性的型別收窄

作為開發者,我們經常需要處理在執行時並不完全確定的值。事實上,無論是從伺服器接收響應還是讀取配置檔案,我們經常不知道屬性是否存在。JavaScript 的 in 運算子可以檢查某個屬性是否存在於物件中。

以前,TypeScript 允許我們收窄(narrow)掉任何未明確列出該屬性的型別。

ts
interface RGB {
red: number;
green: number;
blue: number;
}
interface HSV {
hue: number;
saturation: number;
value: number;
}
function setColor(color: RGB | HSV) {
if ("hue" in color) {
// 'color' now has the type HSV
}
// ...
}

在這裡,型別 RGB 沒有列出 hue,因此被收窄剔除,只剩下型別 HSV

但是,如果沒有任何型別列出某個給定的屬性呢?在這種情況下,語言並沒有提供太多幫助。讓我們看下面這個 JavaScript 示例。

js
function tryGetPackageName(context) {
const packageJSON = context.packageJSON;
// Check to see if we have an object.
if (packageJSON && typeof packageJSON === "object") {
// Check to see if it has a string name property.
if ("name" in packageJSON && typeof packageJSON.name === "string") {
return packageJSON.name;
}
}
return undefined;
}

將其改寫為標準的 TypeScript 只需為 context 定義並使用一個型別;然而,為 packageJSON 屬性選擇一個安全的型別(如 unknown)會在舊版本的 TypeScript 中引發問題。

ts
interface Context {
packageJSON: unknown;
}
function tryGetPackageName(context: Context) {
const packageJSON = context.packageJSON;
// Check to see if we have an object.
if (packageJSON && typeof packageJSON === "object") {
// Check to see if it has a string name property.
if ("name" in packageJSON && typeof packageJSON.name === "string") {
// ~~~~
// error! Property 'name' does not exist on type 'object.
return packageJSON.name;
// ~~~~
// error! Property 'name' does not exist on type 'object.
}
}
return undefined;
}

這是因為雖然 packageJSON 的型別從 unknown 收窄到了 object,但 in 運算子嚴格地收窄至那些實際定義了所檢查屬性的型別。結果,packageJSON 的型別依然保持為 object

TypeScript 4.9 使 in 運算子在收窄那些根本沒有列出該屬性的型別時變得更加強大。語言不再保持其原樣,而是將它們的型別與 Record<"property-key-being-checked", unknown> 進行交叉(intersect)。

所以,在我們的示例中,packageJSON 的型別將從 unknown 收窄為 object,進而收窄為 object & Record<"name", unknown>。這使我們可以直接訪問 packageJSON.name 並對其進行獨立的收窄。

ts
interface Context {
packageJSON: unknown;
}
function tryGetPackageName(context: Context): string | undefined {
const packageJSON = context.packageJSON;
// Check to see if we have an object.
if (packageJSON && typeof packageJSON === "object") {
// Check to see if it has a string name property.
if ("name" in packageJSON && typeof packageJSON.name === "string") {
// Just works!
return packageJSON.name;
}
}
return undefined;
}

TypeScript 4.9 還收緊了對 in 使用方式的一些檢查,確保左側可賦值給 string | number | symbol 型別,右側可賦值給 object。這有助於檢查我們是否使用了有效的屬性鍵,而不是意外地檢查了原始型別。

欲瞭解更多資訊,請閱讀實現該功能的 pull request

類中的自動訪問器 (Auto-Accessors)

TypeScript 4.9 支援 ECMAScript 即將推出的一項特性,即自動訪問器(auto-accessors)。自動訪問器的宣告方式與類中的屬性相同,只是使用了 accessor 關鍵字。

ts
class Person {
accessor name: string;
constructor(name: string) {
this.name = name;
}
}

在底層,這些自動訪問器會“去糖”為帶有不可訪問私有屬性的 getset 訪問器。

ts
class Person {
#__name: string;
get name() {
return this.#__name;
}
set name(value: string) {
this.#__name = value;
}
constructor(name: string) {
this.name = name;
}
}

你可以在原始 PR 中閱讀更多關於自動訪問器 pull request 的資訊

針對 NaN 的相等性檢查

JavaScript 開發者的一個主要坑點是使用內建的相等運算子來檢查 NaN 值。

作為背景,NaN 是一個特殊的數值,代表“非數字”(Not a Number)。沒有任何東西等於 NaN —— 即使是 NaN 本身!

js
console.log(NaN == 0) // false
console.log(NaN === 0) // false
console.log(NaN == NaN) // false
console.log(NaN === NaN) // false

但至少對稱地,任何東西總是與 NaN 不相等。

js
console.log(NaN != 0) // true
console.log(NaN !== 0) // true
console.log(NaN != NaN) // true
console.log(NaN !== NaN) // true

這在技術上並非 JavaScript 特有的問題,因為任何包含 IEEE-754 浮點數的語言都具有相同的行為;但 JavaScript 的主要數值型別是浮點數,且在 JavaScript 中解析數字經常會導致 NaN。因此,檢查 NaN 變得相當普遍,而正確的方法是使用 Number.isNaN —— 正如我們提到的,許多人會不小心使用 someValue === NaN 來檢查。

現在,TypeScript 對與 NaN 的直接比較報錯,並建議改用 Number.isNaN 的某種變體。

ts
function validate(someValue: number) {
return someValue !== NaN;
// ~~~~~~~~~~~~~~~~~
// error: This condition will always return 'true'.
// Did you mean '!Number.isNaN(someValue)'?
}

我們認為這一改變將嚴格有助於捕捉新手錯誤,類似於 TypeScript 目前對物件和陣列字面量比較發出的錯誤警告。

我們衷心感謝 Oleksandr Tarasiuk,他貢獻了這一檢查功能

檔案監控現已使用檔案系統事件

在早期版本中,TypeScript 在監控單個檔案時嚴重依賴輪詢(polling)。使用輪詢策略意味著定期檢查檔案的狀態以檢視更新。在 Node.js 上,fs.watchFile 是獲取輪詢檔案監控器的內建方式。雖然輪詢在跨平臺和檔案系統方面往往更可預測,但這意味著你的 CPU 必須定期被中斷以檢查檔案更新,即使沒有任何變化。對於幾十個檔案,這可能不明顯;但對於大型專案——或 node_modules 中有大量檔案——這可能會成為資源消耗大戶。

總的來說,更好的方法是使用檔案系統事件。無需輪詢,我們可以宣告我們要監控特定檔案的更新,並提供一個在這些檔案確實發生變化時的回撥。大多數現代平臺都提供如 CreateIoCompletionPortkqueueepollinotify 等設施和 API。Node.js 主要透過提供 fs.watch 來抽象這些 API。檔案系統事件通常工作得很好,但在使用 fs.watch API 時有許多注意事項。監控器需要仔細考慮 inode 監控某些檔案系統上的不可用性(例如網路檔案系統)、遞迴檔案監控是否可用、目錄重新命名是否觸發事件,甚至檔案監控器耗盡!換句話說,這並非免費午餐,尤其是如果你追求跨平臺的情況下。

因此,我們的預設選擇是最小公分母:輪詢。雖然不是總是如此,但大多數情況下是這樣的。

隨著時間的推移,我們提供了選擇其他檔案監控策略的手段。這使我們能夠收集反饋並加強我們的檔案監控實現,以應對大多數這些特定於平臺的陷阱。隨著 TypeScript 需要擴充套件到更大的程式碼庫,並且在該領域有所改進,我們認為預設切換到檔案系統事件將是一項值得的投資。

在 TypeScript 4.9 中,檔案監控預設由檔案系統事件驅動,僅在無法設定基於事件的監控器時才回退到輪詢。對於大多數開發者來說,這應該能在執行 --watch 模式或在 Visual Studio 或 VS Code 等 TypeScript 驅動的編輯器中執行時,提供更少資源消耗的體驗。

檔案監控的工作方式仍然可以透過環境變數和 watchOptions 進行配置——並且像 VS Code 這樣的一些編輯器可以獨立支援 watchOptions。原始碼駐留在網路檔案系統(如 NFS 和 SMB)上的開發者可能需要切換回舊的行為;儘管如果伺服器有足夠的處理能力,啟用 SSH 並遠端執行 TypeScript 以便其具有直接的本地檔案訪問許可權可能更好。VS Code 有大量遠端擴充套件來簡化此操作。

你可以在 GitHub 上閱讀有關此更改的更多資訊

編輯器的“刪除未使用的匯入”和“排序匯入”命令

以前,TypeScript 僅支援兩個管理匯入的編輯器命令。對於我們的示例,請看以下程式碼。

ts
import { Zebra, Moose, HoneyBadger } from "./zoo";
import { foo, bar } from "./helper";
let x: Moose | HoneyBadger = foo();

第一個稱為“組織匯入”(Organize Imports),它會刪除未使用的匯入,然後對剩餘的匯入進行排序。它會將該檔案重寫為如下所示。

ts
import { foo } from "./helper";
import { HoneyBadger, Moose } from "./zoo";
let x: Moose | HoneyBadger = foo();

在 TypeScript 4.3 中,我們引入了一個名為“排序匯入”(Sort Imports)的命令,它對檔案中的匯入進行排序,而不刪除它們——並會將檔案重寫為如下所示。

ts
import { bar, foo } from "./helper";
import { HoneyBadger, Moose, Zebra } from "./zoo";
let x: Moose | HoneyBadger = foo();

“排序匯入”的一個注意事項是,在 Visual Studio Code 中,此功能僅作為“儲存時”命令提供,而不是作為可手動觸發的命令。

TypeScript 4.9 添加了另一半功能,現在提供了“刪除未使用的匯入”(Remove Unused Imports)。TypeScript 現在將刪除未使用的匯入名稱和語句,但在其他方面保持相對順序不變。

ts
import { Moose, HoneyBadger } from "./zoo";
import { foo } from "./helper";
let x: Moose | HoneyBadger = foo();

此功能適用於所有希望使用這兩個命令的編輯器;但值得注意的是,Visual Studio Code(1.73 及更高版本)將內建支援透過其命令面板(Command Palette)顯示這些命令。喜歡使用更細粒度的“刪除未使用的匯入”或“排序匯入”命令的使用者,如果需要,可以將“組織匯入”的快捷鍵組合重新分配給它們。

你可以在此處檢視該功能的細節

return 關鍵字上跳轉到定義

在編輯器中,當在 return 關鍵字上執行“跳轉到定義”時,TypeScript 現在會讓你跳轉到相應函式的頂部。這有助於快速瞭解 return 屬於哪個函式。

我們預計 TypeScript 會將此功能擴充套件到更多關鍵字,例如 awaityield,或者switchcasedefault

此功能得以實現要感謝 Oleksandr Tarasiuk

效能改進

TypeScript 包含一些雖小但顯著的效能改進。

首先,TypeScript 的 forEachChild 函式已被重寫,使用函式表查詢來代替跨所有語法節點的 switch 語句。forEachChild 是編譯器中遍歷語法節點的功臣,在編譯器的繫結階段以及語言服務的多個部分被大量使用。forEachChild 的重構使我們的繫結階段和語言服務操作的時間減少了多達 20%。

在我們發現 forEachChild 的效能提升後,我們將其嘗試應用於 visitEachChild,這是我們用於在編譯器和語言服務中轉換節點的函式。同樣的重構使生成專案輸出的時間減少了多達 3%。

forEachChild 的初步探索受到 Artemis Everfree 一篇博文的啟發。雖然我們有理由相信效能提升的根本原因可能與函式大小/複雜性有關,而不是博文中描述的問題,但我們很感謝能夠從中學習並嘗試了一次相對快速的重構,從而使 TypeScript 執行得更快。

最後,TypeScript 在條件型別(conditional type)的 true 分支中儲存型別資訊的方式得到了最佳化。在如下型別中:

ts
interface Zoo<T extends Animal> {
// ...
}
type MakeZoo<A> = A extends Animal ? Zoo<A> : never;

在檢查 Zoo<A> 是否有效時,TypeScript 必須“記住” A 也必須是 Animal。這基本上是透過建立一個特殊的型別來完成的,該型別過去用於儲存 AAnimal 的交集;然而,TypeScript 之前是急切地執行此操作的,這並不總是必要的。此外,我們型別檢查器中的一些錯誤程式碼阻止了這些特殊型別被簡化。現在,TypeScript 會延遲這些型別的交集運算,直到必要時才進行。對於大量使用條件型別的程式碼庫,你可能會見證 TypeScript 的顯著速度提升,但在我們的效能測試套件中,我們觀察到型別檢查時間有了較溫和的 3% 減少。

你可以在各自的 pull request 中閱讀有關這些最佳化的更多資訊。

正確性修復與破壞性更改

lib.d.ts 更新

雖然 TypeScript 努力避免重大破壞,但即使是內建庫中的小改動也可能導致問題。我們預計 DOM 和 lib.d.ts 更新不會導致重大破壞,但可能會有一些微小的破壞。

Promise.resolve 提供更好的型別

Promise.resolve 現在使用 Awaited 型別來解包傳遞給它的 Promise 類型別。這意味著它更經常返回正確的 Promise 型別,但如果現有程式碼期望 anyunknown 而不是 Promise,這種改進後的型別可能會破壞現有程式碼。更多資訊,請參閱最初的更改

JavaScript 發出(emit)不再忽略匯入

當 TypeScript 最初支援 JavaScript 的型別檢查和編譯時,它無意中支援了一個稱為“匯入忽略”(import elision)的特性。簡而言之,如果一個匯入沒有作為值使用,或者編譯器可以檢測到該匯入在執行時不引用值,那麼編譯器就會在輸出中刪除該匯入。

這種行為是有問題的,特別是對於“檢測匯入是否不引用值”這一機制,因為這意味著 TypeScript 必須信任有時不準確的宣告檔案。因此,TypeScript 現在在 JavaScript 檔案中保留匯入。

js
// Input:
import { someValue, SomeClass } from "some-module";
/** @type {SomeClass} */
let val = someValue;
// Previous Output:
import { someValue } from "some-module";
/** @type {SomeClass} */
let val = someValue;
// Current Output:
import { someValue, SomeClass } from "some-module";
/** @type {SomeClass} */
let val = someValue;

更多資訊可在實現該更改的 PR 中獲取。

exports 的優先順序高於 typesVersions

以前,當在 --moduleResolution node16 下透過 package.json 解析時,TypeScript 錯誤地將 typesVersions 欄位的優先順序置於 exports 欄位之上。如果此更改影響了你的庫,你可能需要在 package.jsonexports 欄位中新增 types@ 版本選擇器。

diff
{
"type": "module",
"main": "./dist/main.js"
"typesVersions": {
"<4.8": { ".": ["4.8-types/main.d.ts"] },
"*": { ".": ["modern-types/main.d.ts"] }
},
"exports": {
".": {
+ "types@<4.8": "./4.8-types/main.d.ts",
+ "types": "./modern-types/main.d.ts",
"import": "./dist/main.js"
}
}
}

更多資訊,請參閱此 pull request

SubstitutionType 上的 substituteconstraint 取代

作為對替換型別(substitution types)最佳化的一部分,SubstitutionType 物件不再包含表示有效替換的 substitute 屬性(通常是基型別和隱式約束的交集),而是隻包含 constraint 屬性。

更多詳情,請在原始 pull request 中閱讀

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

此頁面的貢獻者
Nnavya9singh (6)
DSDavid Sherret (1)
BBrennan (1)
MMarius (1)
제스 (1)
1+

最後更新:2026 年 3 月 27 日