想象一下下面的代碼:
declare const untypedCache: Map<any, any>;
function getUrlObject(urlString: string): URL {
return untypedCache.has(urlString) ?
untypedCache.get(urlString) :
urlString;
}
這段代碼的目的是從緩存中獲取 URL 對象,如果不存在就創(chuàng)建一個新的 URL 對象。但這里有個問題:我們忘記用輸入實際構造一個新的 URL 對象了。遺憾的是,TypeScript 通常無法捕捉到這種錯誤。
當 TypeScript 檢查像 cond ? trueBranch : falseBranch
這樣的條件表達式時,它的類型會被視為兩個分支類型的聯(lián)合。換句話說,它會獲取 trueBranch
和 falseBranch
的類型,然后將它們組合成一個聯(lián)合類型。在這個例子中,untypedCache.get(urlString)
的類型是 any
,而 urlString
的類型是 string
。問題就出在這里,因為 any
類型與其他類型交互時會“感染”其他類型。聯(lián)合類型 any | string
會被簡化為 any
,所以當
TypeScript 開始檢查 return
語句中的表達式是否與預期的返回類型 URL
兼容時,類型系統(tǒng)已經丟失了能夠捕捉到此代碼中錯誤的信息。
在 TypeScript 5.8 中,類型系統(tǒng)對直接位于 return
語句內的條件表達式進行了特殊處理。條件的每個分支都會被檢查是否與包含函數(shù)的聲明返回類型(如果存在)兼容,因此類型系統(tǒng)可以捕捉到上面示例中的錯誤。
declare const untypedCache: Map<any, any>;
function getUrlObject(urlString: string): URL {
return untypedCache.has(urlString) ?
untypedCache.get(urlString) :
urlString; // 錯誤!類型“string”不能分配給類型“URL”。
}
這一改動是作為更廣泛的未來改進的一部分在此拉取請求中完成的。
--module nodenext
下支持對 ECMAScript 模塊的 require()
多年來,Node.js 支持 ECMAScript 模塊(ESM)和 CommonJS 模塊共存。但兩者之間的互操作性存在一些挑戰(zhàn):
import
CommonJS 文件require()
ESM 文件換句話說,從 ESM 文件中使用 CommonJS 文件是可行的,但反過來則不行。這給希望提供 ESM 支持的庫作者帶來了許多挑戰(zhàn)。這些庫作者要么不得不與 CommonJS 用戶打破兼容性,要么“雙重發(fā)布”他們的庫(為 ESM 和 CommonJS 提供單獨的入口點),要么無限期地停留在 CommonJS 上。雖然雙重發(fā)布聽起來像是一個不錯的折中方案,但它是一個復雜且容易出錯的過程,還會使包中的代碼量大致翻倍。
Node.js 22 放寬了一些限制,允許從 CommonJS 模塊中 require("esm")
調用 ECMAScript 模塊。Node.js 仍然不允許對包含頂級 await
的 ESM 文件進行 require()
,但大多數(shù)其他 ESM 文件現(xiàn)在可以從 CommonJS 文件中使用。這為庫作者提供了一個重大機會,使他們可以在不雙重發(fā)布庫的情況下提供 ESM 支持。
TypeScript 5.8 在 --module nodenext
標志下支持這種行為。當啟用了 --module nodenext
時,TypeScript 將避免對這些對 ESM 文件的 require()
調用發(fā)出錯誤。
由于此功能可能會回退到 Node.js 的較早版本,目前沒有穩(wěn)定的 --module nodeXXXX
選項啟用此行為;然而,我們預計 TypeScript 的未來版本可以在 node20
下穩(wěn)定此功能。在此期間,我們鼓勵使用 Node.js 22 及更高版本的用戶使用 --module nodenext
,而庫作者和使用較早 Node.js 版本的用戶應繼續(xù)使用 --module node16
(或進行小更新到 --module node18
)。
有關更多信息,請參閱我們對 require("esm") 的支持文檔。
--module node18
TypeScript 5.8 引入了一個穩(wěn)定的 --module node18
標志。對于那些堅持使用 Node.js 18 的用戶,此標志提供了一個穩(wěn)定的參考點,不包含 --module nodenext
中的某些行為。具體來說:
node18
下禁止對 ECMAScript 模塊的 require()
,但在 nodenext
下允許node18
下允許導入斷言(已棄用,改為導入屬性),但在 nodenext
下禁止有關更多信息,請參閱 --module node18
拉取請求以及對 --module nodenext
的更改。
--erasableSyntaxOnly
選項最近,Node.js 23.6 取消了對直接運行 TypeScript 文件的實驗性支持的標記;然而,只有在該模式下支持某些構造。Node.js 取消了對 --experimental-strip-types
的標記,這要求任何 TypeScript 特定的語法不能具有運行時語義。換句話說,必須能夠輕松地“擦除”或“剝離”文件中的任何 TypeScript 特定語法,留下一個有效的 JavaScript 文件。
這意味著像以下這樣的構造是不被支持的:
enum
聲明namespace
和 module
import =
和 export =
賦值這里有一些不工作的示例:
// ? 錯誤:一個 `import ... = require(...)` 別名
import foo = require("foo");
// ? 錯誤:一個具有運行時代碼的命名空間
namespace container {}
// ? 錯誤:一個 `import =` 別名
import Bar = container.Bar;
class Point {
// ? 錯誤:參數(shù)屬性
constructor(public x: number, public y: number) { }
}
// ? 錯誤:一個 `export =` 賦值
export = Point;
// ? 錯誤:一個枚舉聲明
enum Direction {
Up,
Down,
Left,
Right,
}
像 ts-blank-space 或 Amaro(Node.js 中類型剝離的底層庫)這樣的類似工具也有相同的限制。如果這些工具遇到不符合要求的代碼,它們會提供有用的錯誤消息,但你仍然只有在實際嘗試運行代碼時才會發(fā)現(xiàn)代碼不工作。
這就是為什么 TypeScript 5.8 引入了 --erasableSyntaxOnly
標志。當啟用此標志時,TypeScript 會對具有運行時行為的大多數(shù) TypeScript 特定構造發(fā)出錯誤。
class C {
constructor(public x: number) { }
// ~~~~~~~~~~~~~~~~
// 錯誤!當啟用了 'erasableSyntaxOnly' 時,此語法不允許。
}
通常,你會想要將此標志與 --verbatimModuleSyntax
結合使用,這可以確保模塊包含適當?shù)膶胝Z法,并且不會發(fā)生導入省略。
有關更多信息,請參閱此處的實現(xiàn)。
--libReplacement
標志在 TypeScript 4.5 中,我們引入了用自定義文件替換默認 lib
文件的可能性。這是基于從名為 @typescript/lib-*
的包中解析庫文件的可能性。例如,你可以通過以下 package.json
將 dom
庫鎖定到 @types/web
包的特定版本:
{
"devDependencies": {
"@typescript/lib-dom": "npm:@types/web@0.0.199"
}
}
安裝后,應該存在一個名為 @typescript/lib-dom
的包,TypeScript 在 dom
被你的設置隱含時會一直查找它。
這是一個強大的功能,但也增加了一些額外的工作。即使你不使用此功能,TypeScript 也會一直進行此查找,并且必須監(jiān)視 node_modules
中的更改,以防一個 lib
替換包開始存在。
TypeScript 5.8 引入了 --libReplacement
標志,允許你禁用此行為。如果你不使用 --libReplacement
,現(xiàn)在可以使用 --libReplacement false
禁用它。在未來,--libReplacement false
可能會成為默認值,因此如果你目前依賴此行為,應該考慮使用 --libReplacement true
明確啟用它。
有關更多信息,請參閱此處的更改。
為了使類中的計算屬性在聲明文件中具有更可預測的發(fā)出,TypeScript 5.8 將始終保留實體名稱(bareVariables
和 dotted.names.that.look.like.this
)在類中的計算屬性名稱中。
例如,考慮以下代碼:
export let propName = "theAnswer";
export class MyClass {
[propName] = 42;
// ~~~~~~~~~~
// 錯誤!類屬性聲明中的計算屬性名稱必須具有簡單的字面量類型或 'unique symbol' 類型。
}
TypeScript 的早期版本在為此模塊生成聲明文件時會發(fā)出錯誤,并且會生成一個盡力而為的聲明文件,其中包含一個索引簽名。
export declare let propName: string;
export declare class MyClass {
[x: string]: number;
}
在 TypeScript 5.8 中,示例代碼現(xiàn)在被允許,發(fā)出的聲明文件將與你編寫的代碼匹配:
export declare let propName: string;
export declare class MyClass {
[propName]: number;
}
請注意,這不會在類上創(chuàng)建靜態(tài)命名的屬性。你仍然會得到一個實際上像 [x: string]: number
這樣的索引簽名,因此對于這種情況,你需要使用 unique symbol
或字面量類型。
請注意,編寫此代碼在 --isolatedDeclarations
標志下過去和現(xiàn)在都是錯誤;但我們預計,由于此更改,計算屬性名稱通常將被允許在聲明發(fā)出中。
請注意,使用 TypeScript 5.8 編譯的文件可能會生成一個在 TypeScript 5.7 或更早版本中不向后兼容的聲明文件。
有關更多信息,請參閱實現(xiàn)此功能的拉取請求。
TypeScript 5.8 引入了許多優(yōu)化,這些優(yōu)化可以同時改進構建程序的時間,以及在 --watch
模式或編輯器場景中基于文件更改更新程序的時間。
首先,TypeScript 現(xiàn)在避免了在規(guī)范化路徑時涉及的數(shù)組分配。通常,路徑規(guī)范化會將路徑的每個部分分割成一個字符串數(shù)組,根據相對段規(guī)范化路徑,然后使用規(guī)范分隔符將它們重新連接。對于包含許多文件的項目,這可能是一項重要且重復的工作。TypeScript 現(xiàn)在避免分配數(shù)組,而是更直接地在原始路徑的索引上操作。
此外,當進行不會改變項目基本結構的編輯時,TypeScript 現(xiàn)在避免重新驗證提供給它的選項(例如 tsconfig.json
的內容)。這意味著,例如,一個簡單的編輯可能不需要檢查項目的輸出路徑是否與輸入路徑沖突。相反,可以使用上次檢查的結果。這應該使大型項目中的編輯感覺更響應迅速。
這一部分突出了作為任何升級的一部分應該被承認和理解的一組值得注意的更改。有時它會突出棄用、刪除和新限制。它還可以包含功能改進的錯誤修復,但也可能通過引入新錯誤來影響現(xiàn)有構建。
lib.d.ts
DOM 生成的類型可能會對你的代碼庫的類型檢查產生影響。有關更多信息,請參閱與此版本的 TypeScript 相關的 DOM 和 lib.d.ts
更新的問題。
--module nodenext
下對導入斷言的限制導入斷言是 ECMAScript 的提議添加,以確保導入的某些屬性(例如,“此模塊是 JSON,而不是可執(zhí)行的 JavaScript 代碼”)。它們被重新設想為一個名為導入屬性的提議。作為過渡的一部分,它們從使用 assert
關鍵字改為使用 with
關鍵字。
// 一個導入斷言 ? - 與大多數(shù)運行時不兼容。
import data from "./data.json" assert { type: "json" };
// 一個導入屬性 ? - 導入 JSON 文件的推薦方式。
import data from "./data.json" with { type: "json" };
Node.js 22 不再接受使用 assert
語法的導入斷言。因此,當在 TypeScript 5.8 中啟用 --module nodenext
時,如果遇到導入斷言,TypeScript 將發(fā)出錯誤。
import data from "./data.json" assert { type: "json" };
// ~~~~~~
// 錯誤!導入斷言已被導入屬性取代。請使用 'with' 而不是 'assert'。
有關更多信息,請參閱此處的更改。
更多建議: