TypeScript 5.8

返回表達(dá)式中分支的細(xì)粒度檢查(Granular Checks for Branches in Return Expressions)

來(lái)看這樣一段代碼:

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

這段代碼的意圖是:如果緩存中存在某個(gè) URL 對(duì)應(yīng)的對(duì)象,就從緩存中獲取;否則就創(chuàng)建一個(gè)新的 URL 對(duì)象。但這里有個(gè) bug:我們忘了在 else 分支中真正用輸入構(gòu)造一個(gè)新的 URL 對(duì)象。

不幸的是,TypeScript 過(guò)去通常無(wú)法捕捉到這種錯(cuò)誤。

當(dāng) TypeScript 檢查像 cond ? trueBranch : falseBranch 這樣的條件表達(dá)式時(shí),它會(huì)將整個(gè)表達(dá)式的類(lèi)型視為兩個(gè)分支類(lèi)型的聯(lián)合類(lèi)型。也就是說(shuō),它獲取 trueBranchfalseBranch 的類(lèi)型,并將它們合并為一個(gè)聯(lián)合類(lèi)型。在這個(gè)例子中,untypedCache.get(urlString) 的類(lèi)型是 any,而 urlString 的類(lèi)型是 string。問(wèn)題就出在這里:any 類(lèi)型在與其他類(lèi)型交互時(shí)具有“傳染性”。聯(lián)合類(lèi)型 any | string 會(huì)被簡(jiǎn)化為 any。所以當(dāng) TypeScript 最終檢查 return 語(yǔ)句的表達(dá)式是否符合函數(shù)聲明的返回類(lèi)型 URL 時(shí),類(lèi)型系統(tǒng)已經(jīng)失去了識(shí)別這個(gè) bug 的能力。

在 TypeScript 5.8 中,類(lèi)型系統(tǒng)對(duì)直接位于 return 語(yǔ)句中的條件表達(dá)式進(jìn)行了特殊處理。條件的每個(gè)分支都會(huì)與包含該函數(shù)的聲明返回類(lèi)型(如果有)進(jìn)行對(duì)比,因此能夠識(shí)別出上面的代碼中的錯(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'。
}

這個(gè)改動(dòng)是通過(guò)這個(gè) Pull Request 引入的,它是對(duì) TypeScript 類(lèi)型系統(tǒng)未來(lái)一系列改進(jìn)的一部分。

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

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

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

換句話(huà)說(shuō),從 ESM 引入 CommonJS 是可行的,但反過(guò)來(lái)卻不行。這為那些希望支持 ESM 的庫(kù)作者帶來(lái)了很多麻煩。他們要么需要放棄對(duì) CommonJS 用戶(hù)的支持,要么進(jìn)行“雙重發(fā)布”(為 ESM 和 CommonJS 分別提供入口文件),要么干脆一直停留在 CommonJS 上。雖然“雙重發(fā)布”聽(tīng)起來(lái)像是一個(gè)折中方案,但它是一個(gè)復(fù)雜且容易出錯(cuò)的過(guò)程,并且會(huì)讓包的體積幾乎翻倍。

Node.js 22 放寬了這些限制,允許 CommonJS 模塊中使用 require("esm") 引入 ECMAScript 模塊。雖然仍不支持引入包含頂層 await 的 ESM 文件,但大多數(shù)其他 ESM 文件現(xiàn)在都可以被 CommonJS 文件引入。這為庫(kù)作者帶來(lái)了重大機(jī)遇:他們可以支持 ESM 而無(wú)需雙重發(fā)布。

TypeScript 5.8 在 --module nodenext 模式下支持這一行為。當(dāng)啟用 --module nodenext 時(shí),TypeScript 不會(huì)對(duì)這些 require() 引入 ESM 的調(diào)用報(bào)錯(cuò)。

由于該功能可能會(huì)被回溯性支持到舊版 Node.js,目前還沒(méi)有一個(gè)穩(wěn)定的 --module nodeXXXX 可用于開(kāi)啟該行為;不過(guò)我們預(yù)計(jì)未來(lái)版本的 TypeScript 可能會(huì)在 node20 模式下穩(wěn)定此特性。在此之前,我們建議使用 Node.js 22 或更新版本的用戶(hù)啟用 --module nodenext,而庫(kù)作者或使用舊版 Node.js 的用戶(hù)則繼續(xù)使用 --module node16,或升級(jí)到 --module node18。

更多信息詳見(jiàn):require("esm") 的支持說(shuō)明。

--module node18

TypeScript 5.8 新增了一個(gè)穩(wěn)定的 --module node18 標(biāo)志。對(duì)于堅(jiān)持使用 Node.js 18 的用戶(hù)來(lái)說(shuō),這個(gè)標(biāo)志提供了一個(gè)不包含 --module nodenext 某些行為的穩(wěn)定選擇。具體區(qū)別如下:

  • node18不允許 使用 require() 引入 ESM 模塊;而在 nodenext 下是允許的
  • import 斷言(已被 import attributes 取代)在 node18 下仍被支持;但在 nodenext 中則不支持

詳細(xì)內(nèi)容可見(jiàn):


--erasableSyntaxOnly 選項(xiàng)

最近,Node.js 23.6 取消了對(duì)直接運(yùn)行 TypeScript 文件的實(shí)驗(yàn)性支持的限制;但只有部分語(yǔ)法在該模式下被支持。

Node.js 目前提供了一個(gè)名為 --experimental-strip-types 的模式,它要求 TypeScript 的語(yǔ)法不能帶有運(yùn)行時(shí)語(yǔ)義。換句話(huà)說(shuō),必須可以輕松地“擦除”任何 TypeScript 專(zhuān)屬語(yǔ)法,使剩下的代碼是合法的 JavaScript。

這意味著以下語(yǔ)法結(jié)構(gòu)不被支持

  • enum 聲明
  • 含有運(yùn)行時(shí)代碼的 namespacemodule
  • 類(lèi)中的參數(shù)屬性(parameter properties)
  • 非 ECMAScript 風(fēng)格的 import =export =

以下是一些不被支持的示例:

enum Color {
    Red,
    Green,
    Blue
}

namespace MyNamespace {
    export const value = 42;
}

class Person {
    constructor(public name: string) {} // 參數(shù)屬性
}

import foo = require("foo");

這些語(yǔ)法在 --erasableSyntaxOnly 或 Node.js 的 --experimental-strip-types 模式下將會(huì)導(dǎo)致錯(cuò)誤。

以下是你提供內(nèi)容的完整中文翻譯:


? 錯(cuò)誤示例:

// ? 錯(cuò)誤:`import ... = require(...)` 的別名寫(xiě)法
import foo = require("foo");

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

// ? 錯(cuò)誤:`import =` 的別名寫(xiě)法
import Bar = container.Bar;

class Point {
    // ? 錯(cuò)誤:構(gòu)造函數(shù)中的參數(shù)屬性寫(xiě)法
    constructor(public x: number, public y: number) { }
}

// ? 錯(cuò)誤:`export =` 的導(dǎo)出方式
export = Point;

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

類(lèi)似的工具,比如 ts-blank-space 或 Node.js 中用于類(lèi)型剝離的底層庫(kù) Amaro,也有相同的限制。這些工具在遇到不符合要求的代碼時(shí)會(huì)給出友好的錯(cuò)誤信息,但你仍然需要真正運(yùn)行代碼才能發(fā)現(xiàn)這些問(wèn)題。

因此,TypeScript 5.8 引入了 --erasableSyntaxOnly 選項(xiàng)。當(dāng)啟用該選項(xiàng)時(shí),TypeScript 會(huì)對(duì)大多數(shù)具有運(yùn)行時(shí)代碼語(yǔ)義的 TypeScript 特有語(yǔ)法報(bào)錯(cuò)。

class C {
    constructor(public x: number) { }
    //          ~~~~~~~~~~~~~~~~
    // 錯(cuò)誤!啟用 'erasableSyntaxOnly' 時(shí)不允許使用該語(yǔ)法。
}

通常你會(huì)希望將此選項(xiàng)與 --verbatimModuleSyntax 結(jié)合使用,以確保模塊使用的是正確的導(dǎo)入語(yǔ)法,并且不會(huì)發(fā)生導(dǎo)入消除(import elision)。

?? 更多信息可見(jiàn)實(shí)現(xiàn) PR。

--libReplacement 標(biāo)志

在 TypeScript 4.5 中,我們引入了用自定義 lib 文件替代默認(rèn) lib 文件的能力。這是通過(guò)從名為 @typescript/lib-* 的包中解析庫(kù)文件實(shí)現(xiàn)的。例如,你可以通過(guò)如下方式將 DOM 庫(kù)鎖定到特定版本的 @types/web 包:

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

當(dāng)安裝好以后,TypeScript 將查找名為 @typescript/lib-dom 的包(如果 lib 設(shè)置中使用了 dom),無(wú)論你是否使用了該功能,TypeScript 都會(huì)默認(rèn)執(zhí)行這個(gè)查找,并監(jiān)聽(tīng) node_modules 的變化。

在 TypeScript 5.8 中引入了 --libReplacement 標(biāo)志,你可以使用 --libReplacement false 來(lái)關(guān)閉這種行為。如果你依賴(lài)這種替換機(jī)制,請(qǐng)明確啟用它,例如使用 --libReplacement true。未來(lái) false 可能會(huì)成為默認(rèn)值。

?? 詳情見(jiàn)更新內(nèi)容。

聲明文件中的計(jì)算屬性名保留行為

為使計(jì)算屬性在聲明文件中的輸出更可預(yù)測(cè),TypeScript 5.8 將在類(lèi)的計(jì)算屬性中一致保留實(shí)體名(如 bareVariablesdotted.names.that.look.like.this)。

例如,考慮以下代碼:

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

在舊版本中,TypeScript 會(huì)報(bào)錯(cuò),并嘗試生成如下 best-effort 的聲明文件:

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

而在 TypeScript 5.8 中,這段代碼將被允許,并生成如下聲明文件:

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

注意,這不會(huì)創(chuàng)建靜態(tài)命名屬性,而仍然相當(dāng)于 [x: string]: number 的形式。如果你需要靜態(tài)屬性,應(yīng)該使用 unique symbol 或字面量類(lèi)型。

?? 使用 --isolatedDeclarations 時(shí)此類(lèi)代碼仍然會(huì)報(bào)錯(cuò)。但我們預(yù)計(jì),隨著這一變化的引入,計(jì)算屬性名將更廣泛地在聲明文件中被允許。

?? 詳情見(jiàn)實(shí)現(xiàn) PR。

程序加載與更新優(yōu)化

TypeScript 5.8 引入了多項(xiàng)優(yōu)化,提升構(gòu)建程序及響應(yīng)文件變更(如 --watch 模式或編輯器場(chǎng)景)時(shí)的性能:

  • 路徑歸一化優(yōu)化:不再通過(guò)字符串?dāng)?shù)組操作來(lái)處理路徑,而是直接基于索引操作原始路徑,避免了大量數(shù)組創(chuàng)建和連接。
  • 配置項(xiàng)緩存:對(duì)于不改變項(xiàng)目結(jié)構(gòu)的編輯,TypeScript 不再重復(fù)驗(yàn)證如 tsconfig.json 的配置,而是復(fù)用上一次的校驗(yàn)結(jié)果,從而提高響應(yīng)速度,尤其在大型項(xiàng)目中更為明顯。

行為變更匯總(需關(guān)注的重要變更)

  • lib.d.ts 更新:DOM 類(lèi)型變更可能會(huì)影響你代碼庫(kù)的類(lèi)型檢查。請(qǐng)參見(jiàn)相關(guān) issue 獲取更多細(xì)節(jié)。

--module nodenext 下的 import 斷言限制

ECMAScript 曾提出 import assertions,用于確保導(dǎo)入模塊的某些特性(例如確保是 JSON 文件)。后來(lái)該提議演變成 import attributes,并用 with 替換了 assert 關(guān)鍵字:

// ? import assertion(舊提案寫(xiě)法,不兼容)
import data from "./data.json" assert { type: "json" };

// ? import attribute(新提案寫(xiě)法)
import data from "./data.json" with { type: "json" };

Node.js 22 不再支持 assert 語(yǔ)法的 import 斷言。因此,在 TypeScript 5.8 中,當(dāng)啟用了 --module nodenext,使用舊寫(xiě)法將會(huì)報(bào)錯(cuò):

import data from "./data.json" assert { type: "json" };
//                             ~~~~~~
// 錯(cuò)誤!import assertions 已被 import attributes 取代,請(qǐng)使用 'with' 替代 'assert'。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容