TypeScript 5.8 新特性詳解

2025-03-25 14:14 更新

返回表達(dá)式分支的細(xì)致檢查

想象一下下面的代碼:

declare const untypedCache: Map<any, any>;
function getUrlObject(urlString: string): URL {
    return untypedCache.has(urlString) ?
        untypedCache.get(urlString) :
        urlString;
}

這段代碼的目的是從緩存中獲取 URL 對(duì)象,如果不存在就創(chuàng)建一個(gè)新的 URL 對(duì)象。但這里有個(gè)問(wèn)題:我們忘記用輸入實(shí)際構(gòu)造一個(gè)新的 URL 對(duì)象了。遺憾的是,TypeScript 通常無(wú)法捕捉到這種錯(cuò)誤。

當(dāng) TypeScript 檢查像 cond ? trueBranch : falseBranch 這樣的條件表達(dá)式時(shí),它的類(lèi)型會(huì)被視為兩個(gè)分支類(lèi)型的聯(lián)合。換句話說(shuō),它會(huì)獲取 trueBranchfalseBranch 的類(lèi)型,然后將它們組合成一個(gè)聯(lián)合類(lèi)型。在這個(gè)例子中,untypedCache.get(urlString) 的類(lèi)型是 any,而 urlString 的類(lèi)型是 string。問(wèn)題就出在這里,因?yàn)?any 類(lèi)型與其他類(lèi)型交互時(shí)會(huì)“感染”其他類(lèi)型。聯(lián)合類(lèi)型 any | string 會(huì)被簡(jiǎn)化為 any,所以當(dāng) TypeScript 開(kāi)始檢查 return 語(yǔ)句中的表達(dá)式是否與預(yù)期的返回類(lèi)型 URL 兼容時(shí),類(lèi)型系統(tǒng)已經(jīng)丟失了能夠捕捉到此代碼中錯(cuò)誤的信息。

在 TypeScript 5.8 中,類(lèi)型系統(tǒng)對(duì)直接位于 return 語(yǔ)句內(nèi)的條件表達(dá)式進(jìn)行了特殊處理。條件的每個(gè)分支都會(huì)被檢查是否與包含函數(shù)的聲明返回類(lèi)型(如果存在)兼容,因此類(lèi)型系統(tǒng)可以捕捉到上面示例中的錯(cuò)誤。

declare const untypedCache: Map<any, any>;
function getUrlObject(urlString: string): URL {
    return untypedCache.has(urlString) ?
        untypedCache.get(urlString) :
        urlString; // 錯(cuò)誤!類(lèi)型“string”不能分配給類(lèi)型“URL”。
}

這一改動(dòng)是作為更廣泛的未來(lái)改進(jìn)的一部分在此拉取請(qǐng)求中完成的。


--module nodenext 下支持對(duì) ECMAScript 模塊的 require()

多年來(lái),Node.js 支持 ECMAScript 模塊(ESM)和 CommonJS 模塊共存。但兩者之間的互操作性存在一些挑戰(zhàn):

  • ESM 文件可以 import CommonJS 文件
  • CommonJS 文件 不能 require() ESM 文件

換句話說(shuō),從 ESM 文件中使用 CommonJS 文件是可行的,但反過(guò)來(lái)則不行。這給希望提供 ESM 支持的庫(kù)作者帶來(lái)了許多挑戰(zhàn)。這些庫(kù)作者要么不得不與 CommonJS 用戶(hù)打破兼容性,要么“雙重發(fā)布”他們的庫(kù)(為 ESM 和 CommonJS 提供單獨(dú)的入口點(diǎn)),要么無(wú)限期地停留在 CommonJS 上。雖然雙重發(fā)布聽(tīng)起來(lái)像是一個(gè)不錯(cuò)的折中方案,但它是一個(gè)復(fù)雜且容易出錯(cuò)的過(guò)程,還會(huì)使包中的代碼量大致翻倍。

Node.js 22 放寬了一些限制,允許從 CommonJS 模塊中 require("esm") 調(diào)用 ECMAScript 模塊。Node.js 仍然不允許對(duì)包含頂級(jí) await 的 ESM 文件進(jìn)行 require(),但大多數(shù)其他 ESM 文件現(xiàn)在可以從 CommonJS 文件中使用。這為庫(kù)作者提供了一個(gè)重大機(jī)會(huì),使他們可以在不雙重發(fā)布庫(kù)的情況下提供 ESM 支持。

TypeScript 5.8 在 --module nodenext 標(biāo)志下支持這種行為。當(dāng)啟用了 --module nodenext 時(shí),TypeScript 將避免對(duì)這些對(duì) ESM 文件的 require() 調(diào)用發(fā)出錯(cuò)誤。

由于此功能可能會(huì)回退到 Node.js 的較早版本,目前沒(méi)有穩(wěn)定的 --module nodeXXXX 選項(xiàng)啟用此行為;然而,我們預(yù)計(jì) TypeScript 的未來(lái)版本可以在 node20 下穩(wěn)定此功能。在此期間,我們鼓勵(lì)使用 Node.js 22 及更高版本的用戶(hù)使用 --module nodenext,而庫(kù)作者和使用較早 Node.js 版本的用戶(hù)應(yīng)繼續(xù)使用 --module node16(或進(jìn)行小更新到 --module node18)。

有關(guān)更多信息,請(qǐng)參閱我們對(duì) require("esm") 的支持文檔。

--module node18

TypeScript 5.8 引入了一個(gè)穩(wěn)定的 --module node18 標(biāo)志。對(duì)于那些堅(jiān)持使用 Node.js 18 的用戶(hù),此標(biāo)志提供了一個(gè)穩(wěn)定的參考點(diǎn),不包含 --module nodenext 中的某些行為。具體來(lái)說(shuō):

  • node18 下禁止對(duì) ECMAScript 模塊的 require(),但在 nodenext 下允許
  • node18 下允許導(dǎo)入斷言(已棄用,改為導(dǎo)入屬性),但在 nodenext 下禁止

有關(guān)更多信息,請(qǐng)參閱 --module node18 拉取請(qǐng)求以及對(duì) --module nodenext 的更改。


--erasableSyntaxOnly 選項(xiàng)

最近,Node.js 23.6 取消了對(duì)直接運(yùn)行 TypeScript 文件的實(shí)驗(yàn)性支持的標(biāo)記;然而,只有在該模式下支持某些構(gòu)造。Node.js 取消了對(duì) --experimental-strip-types 的標(biāo)記,這要求任何 TypeScript 特定的語(yǔ)法不能具有運(yùn)行時(shí)語(yǔ)義。換句話說(shuō),必須能夠輕松地“擦除”或“剝離”文件中的任何 TypeScript 特定語(yǔ)法,留下一個(gè)有效的 JavaScript 文件。

這意味著像以下這樣的構(gòu)造是不被支持的:

  • enum 聲明
  • 具有運(yùn)行時(shí)代碼的 namespacemodule
  • 類(lèi)中的參數(shù)屬性
  • 非 ECMAScript import =export = 賦值

這里有一些不工作的示例:

// ? 錯(cuò)誤:一個(gè) `import ... = require(...)` 別名
import foo = require("foo");

// ? 錯(cuò)誤:一個(gè)具有運(yùn)行時(shí)代碼的命名空間
namespace container {}

// ? 錯(cuò)誤:一個(gè) `import =` 別名
import Bar = container.Bar;

class Point {
    // ? 錯(cuò)誤:參數(shù)屬性
    constructor(public x: number, public y: number) { }
}

// ? 錯(cuò)誤:一個(gè) `export =` 賦值
export = Point;

// ? 錯(cuò)誤:一個(gè)枚舉聲明
enum Direction {
    Up,
    Down,
    Left,
    Right,
}

像 ts-blank-space 或 Amaro(Node.js 中類(lèi)型剝離的底層庫(kù))這樣的類(lèi)似工具也有相同的限制。如果這些工具遇到不符合要求的代碼,它們會(huì)提供有用的錯(cuò)誤消息,但你仍然只有在實(shí)際嘗試運(yùn)行代碼時(shí)才會(huì)發(fā)現(xiàn)代碼不工作。

這就是為什么 TypeScript 5.8 引入了 --erasableSyntaxOnly 標(biāo)志。當(dāng)啟用此標(biāo)志時(shí),TypeScript 會(huì)對(duì)具有運(yùn)行時(shí)行為的大多數(shù) TypeScript 特定構(gòu)造發(fā)出錯(cuò)誤。

class C {
    constructor(public x: number) { }
    //          ~~~~~~~~~~~~~~~~
    // 錯(cuò)誤!當(dāng)啟用了 'erasableSyntaxOnly' 時(shí),此語(yǔ)法不允許。
}

通常,你會(huì)想要將此標(biāo)志與 --verbatimModuleSyntax 結(jié)合使用,這可以確保模塊包含適當(dāng)?shù)膶?dǎo)入語(yǔ)法,并且不會(huì)發(fā)生導(dǎo)入省略。

有關(guān)更多信息,請(qǐng)參閱此處的實(shí)現(xiàn)。


--libReplacement 標(biāo)志

在 TypeScript 4.5 中,我們引入了用自定義文件替換默認(rèn) lib 文件的可能性。這是基于從名為 @typescript/lib-* 的包中解析庫(kù)文件的可能性。例如,你可以通過(guò)以下 package.jsondom 庫(kù)鎖定到 @types/web 包的特定版本:

{
    "devDependencies": {
        "@typescript/lib-dom": "npm:@types/web@0.0.199"
    }
}

安裝后,應(yīng)該存在一個(gè)名為 @typescript/lib-dom 的包,TypeScript 在 dom 被你的設(shè)置隱含時(shí)會(huì)一直查找它。

這是一個(gè)強(qiáng)大的功能,但也增加了一些額外的工作。即使你不使用此功能,TypeScript 也會(huì)一直進(jìn)行此查找,并且必須監(jiān)視 node_modules 中的更改,以防一個(gè) lib 替換包開(kāi)始存在。

TypeScript 5.8 引入了 --libReplacement 標(biāo)志,允許你禁用此行為。如果你不使用 --libReplacement,現(xiàn)在可以使用 --libReplacement false 禁用它。在未來(lái),--libReplacement false 可能會(huì)成為默認(rèn)值,因此如果你目前依賴(lài)此行為,應(yīng)該考慮使用 --libReplacement true 明確啟用它。

有關(guān)更多信息,請(qǐng)參閱此處的更改。


在聲明文件中保留計(jì)算屬性名稱(chēng)

為了使類(lèi)中的計(jì)算屬性在聲明文件中具有更可預(yù)測(cè)的發(fā)出,TypeScript 5.8 將始終保留實(shí)體名稱(chēng)(bareVariablesdotted.names.that.look.like.this)在類(lèi)中的計(jì)算屬性名稱(chēng)中。

例如,考慮以下代碼:

export let propName = "theAnswer";
export class MyClass {
    [propName] = 42;
    //  ~~~~~~~~~~
    // 錯(cuò)誤!類(lèi)屬性聲明中的計(jì)算屬性名稱(chēng)必須具有簡(jiǎn)單的字面量類(lèi)型或 'unique symbol' 類(lèi)型。
}

TypeScript 的早期版本在為此模塊生成聲明文件時(shí)會(huì)發(fā)出錯(cuò)誤,并且會(huì)生成一個(gè)盡力而為的聲明文件,其中包含一個(gè)索引簽名。

export declare let propName: string;
export declare class MyClass {
    [x: string]: number;
}

在 TypeScript 5.8 中,示例代碼現(xiàn)在被允許,發(fā)出的聲明文件將與你編寫(xiě)的代碼匹配:

export declare let propName: string;
export declare class MyClass {
    [propName]: number;
}

請(qǐng)注意,這不會(huì)在類(lèi)上創(chuàng)建靜態(tài)命名的屬性。你仍然會(huì)得到一個(gè)實(shí)際上像 [x: string]: number 這樣的索引簽名,因此對(duì)于這種情況,你需要使用 unique symbol 或字面量類(lèi)型。

請(qǐng)注意,編寫(xiě)此代碼在 --isolatedDeclarations 標(biāo)志下過(guò)去和現(xiàn)在都是錯(cuò)誤;但我們預(yù)計(jì),由于此更改,計(jì)算屬性名稱(chēng)通常將被允許在聲明發(fā)出中。

請(qǐng)注意,使用 TypeScript 5.8 編譯的文件可能會(huì)生成一個(gè)在 TypeScript 5.7 或更早版本中不向后兼容的聲明文件。

有關(guān)更多信息,請(qǐng)參閱實(shí)現(xiàn)此功能的拉取請(qǐng)求。


在程序加載和更新上的優(yōu)化

TypeScript 5.8 引入了許多優(yōu)化,這些優(yōu)化可以同時(shí)改進(jìn)構(gòu)建程序的時(shí)間,以及在 --watch 模式或編輯器場(chǎng)景中基于文件更改更新程序的時(shí)間。

首先,TypeScript 現(xiàn)在避免了在規(guī)范化路徑時(shí)涉及的數(shù)組分配。通常,路徑規(guī)范化會(huì)將路徑的每個(gè)部分分割成一個(gè)字符串?dāng)?shù)組,根據(jù)相對(duì)段規(guī)范化路徑,然后使用規(guī)范分隔符將它們重新連接。對(duì)于包含許多文件的項(xiàng)目,這可能是一項(xiàng)重要且重復(fù)的工作。TypeScript 現(xiàn)在避免分配數(shù)組,而是更直接地在原始路徑的索引上操作。

此外,當(dāng)進(jìn)行不會(huì)改變項(xiàng)目基本結(jié)構(gòu)的編輯時(shí),TypeScript 現(xiàn)在避免重新驗(yàn)證提供給它的選項(xiàng)(例如 tsconfig.json 的內(nèi)容)。這意味著,例如,一個(gè)簡(jiǎn)單的編輯可能不需要檢查項(xiàng)目的輸出路徑是否與輸入路徑?jīng)_突。相反,可以使用上次檢查的結(jié)果。這應(yīng)該使大型項(xiàng)目中的編輯感覺(jué)更響應(yīng)迅速。


值得注意的行為更改

這一部分突出了作為任何升級(jí)的一部分應(yīng)該被承認(rèn)和理解的一組值得注意的更改。有時(shí)它會(huì)突出棄用、刪除和新限制。它還可以包含功能改進(jìn)的錯(cuò)誤修復(fù),但也可能通過(guò)引入新錯(cuò)誤來(lái)影響現(xiàn)有構(gòu)建。

lib.d.ts

DOM 生成的類(lèi)型可能會(huì)對(duì)你的代碼庫(kù)的類(lèi)型檢查產(chǎn)生影響。有關(guān)更多信息,請(qǐng)參閱與此版本的 TypeScript 相關(guān)的 DOM 和 lib.d.ts 更新的問(wèn)題。


--module nodenext 下對(duì)導(dǎo)入斷言的限制

導(dǎo)入斷言是 ECMAScript 的提議添加,以確保導(dǎo)入的某些屬性(例如,“此模塊是 JSON,而不是可執(zhí)行的 JavaScript 代碼”)。它們被重新設(shè)想為一個(gè)名為導(dǎo)入屬性的提議。作為過(guò)渡的一部分,它們從使用 assert 關(guān)鍵字改為使用 with 關(guān)鍵字。

// 一個(gè)導(dǎo)入斷言 ? - 與大多數(shù)運(yùn)行時(shí)不兼容。
import data from "./data.json" assert { type: "json" };

// 一個(gè)導(dǎo)入屬性 ? - 導(dǎo)入 JSON 文件的推薦方式。
import data from "./data.json" with { type: "json" };

Node.js 22 不再接受使用 assert 語(yǔ)法的導(dǎo)入斷言。因此,當(dāng)在 TypeScript 5.8 中啟用 --module nodenext 時(shí),如果遇到導(dǎo)入斷言,TypeScript 將發(fā)出錯(cuò)誤。

import data from "./data.json" assert { type: "json" };
//                             ~~~~~~
// 錯(cuò)誤!導(dǎo)入斷言已被導(dǎo)入屬性取代。請(qǐng)使用 'with' 而不是 'assert'。

有關(guān)更多信息,請(qǐng)參閱此處的更改。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)