檢查從未初始化的變數
長期以來,TypeScript 一直能夠捕獲變數在所有先前的分支中尚未初始化的相關問題。
tslet result: numberif (someCondition()) {result = doSomeWork();}else {let temporaryWork = doSomeWork();temporaryWork *= 2;// forgot to assign to 'result'}console.log(result); // error: Variable 'result' is used before being assigned.
遺憾的是,在某些情況下此分析並不起作用。例如,如果變數是在一個單獨的函式中被訪問的,型別系統就無法知道該函式何時會被呼叫,因此會採取一種“樂觀”的觀點,認為該變數已被初始化。
tsfunction foo() {let result: numberif (someCondition()) {result = doSomeWork();}else {let temporaryWork = doSomeWork();temporaryWork *= 2;// forgot to assign to 'result'}printResult();function printResult() {console.log(result); // no error here.}}
雖然 TypeScript 5.7 對於那些可能已初始化的變數仍然保持寬容,但型別系統現在能夠在變數從未初始化的情況下報告錯誤。
tsfunction foo() {let result: number// do work, but forget to assign to 'result'function printResult() {console.log(result); // error: Variable 'result' is used before being assigned.}}
相對路徑的路徑重寫
有多種工具和執行時允許你“就地”(in-place)執行 TypeScript 程式碼,這意味著它們不需要生成輸出 JavaScript 檔案的構建步驟。例如,ts-node、tsx、Deno 和 Bun 都支援直接執行 .ts 檔案。最近,Node.js 也在探索透過 --experimental-strip-types(很快將移除標誌!)和 --experimental-transform-types 來實現此類支援。這非常方便,因為它讓我們能夠更快地迭代,而無需擔心重新執行構建任務。
不過,在使用這些模式時需要注意一些複雜性。為了與所有這些工具實現最大程度的相容,在執行時“就地”匯入的 TypeScript 檔案必須使用相應的 TypeScript 副檔名匯入。例如,要匯入一個名為 foo.ts 的檔案,在使用 Node 的新實驗性支援時,我們必須這樣寫:
ts// main.tsimport * as foo from "./foo.ts"; // <- we need foo.ts here, not foo.js
通常情況下,如果我們這樣做,TypeScript 會報錯,因為它期望我們匯入的是輸出檔案。由於一些工具確實允許匯入 .ts,TypeScript 此前已透過一個名為 --allowImportingTsExtensions 的選項支援了這種匯入風格。這工作得很好,但如果我們需要從這些 .ts 檔案中實際生成 .js 檔案呢?這是那些需要分發純 .js 檔案的庫作者的需求,但到目前為止,TypeScript 一直避免重寫任何路徑。
為了支援這種情況,我們添加了一個新的編譯器選項 --rewriteRelativeImportExtensions。當匯入路徑為相對路徑(以 ./ 或 ../ 開頭)、以 TypeScript 副檔名結尾(.ts, .tsx, .mts, .cts)且為非宣告檔案時,編譯器會將路徑重寫為相應的 JavaScript 副檔名(.js, .jsx, .mjs, .cjs)。
ts// Under --rewriteRelativeImportExtensions...// these will be rewritten.import * as foo from "./foo.ts";import * as bar from "../someFolder/bar.mts";// these will NOT be rewritten in any way.import * as a from "./foo";import * as b from "some-package/file.ts";import * as c from "@some-scope/some-package/file.ts";import * as d from "#/file.ts";import * as e from "./file.js";
這使我們能夠編寫可以就地執行的 TypeScript 程式碼,並在準備好後編譯成 JavaScript。
我們注意到,TypeScript 通常避免重寫路徑。原因有幾個,但最明顯的一個是動態匯入。如果開發者寫了如下程式碼,處理 import 接收到的路徑並非易事。事實上,在任何依賴項中覆蓋 import 的行為都是不可能的。
tsfunction getPath() {if (Math.random() < 0.5) {return "./foo.ts";}else {return "./foo.js";}}let myImport = await import(getPath());
另一個問題是(如上所述)只有相對路徑會被重寫,而且它們是“簡單地”重寫的。這意味著任何依賴於 TypeScript 的 baseUrl 和 paths 的路徑都不會被重寫。
json// tsconfig.json{"compilerOptions": {"module": "nodenext",// ..."paths": {"@/*": ["./src/*"]}}}
ts// Won't be transformed, won't work.import * as utilities from "@/utilities.ts";
任何可能透過 package.json 的 exports 和 imports 欄位解析的路徑也不會被重寫。
json// package.json{"name": "my-package","imports": {"#root/*": "./dist/*"}}
ts// Won't be transformed, won't work.import * as utilities from "#root/utilities.ts";
因此,如果你一直使用多個包相互引用的工作區風格佈局,你可能需要使用帶作用域自定義條件的條件匯出來使此功能生效。
json// my-package/package.json{"name": "my-package","exports": {".": {"@my-package/development": "./src/index.ts","import": "./lib/index.js"},"./*": {"@my-package/development": "./src/*.ts","import": "./lib/*.js"}}}
每當你想匯入 .ts 檔案時,你可以使用 node --conditions=@my-package/development 執行它。
請注意我們為 @my-package/development 條件使用的“名稱空間”或“作用域”。這是一種權宜之計,旨在避免可能同樣使用 development 條件的依賴項產生衝突。如果每個人都在他們的包中釋出了一個 development 條件,那麼解析過程可能會嘗試解析為一個 .ts 檔案,而這未必可行。這個想法類似於 Colin McDonnell 在其文章TypeScript monorepo 中的即時型別中所描述的,以及 tshy 載入原始碼的指南。
有關此功能工作原理的更多詳情,請參閱此處的更改。
支援 --target es2024 和 --lib es2024
TypeScript 5.7 現在支援 --target es2024,允許使用者針對 ECMAScript 2024 執行時進行開發。此目標主要用於指定新的 --lib es2024,其中包含 SharedArrayBuffer 和 ArrayBuffer、Object.groupBy、Map.groupBy、Promise.withResolvers 等的多項特性。它還將 Atomics.waitAsync 從 --lib es2022 移至 --lib es2024。
請注意,作為 SharedArrayBuffer 和 ArrayBuffer 更改的一部分,兩者現在有所分歧。為了彌合這一差距並保留底層緩衝區型別,所有 TypedArrays(如 Uint8Array 等)現在也變成了泛型。
tsinterface Uint8Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike> {// ...}
每個 TypedArray 現在都包含一個名為 TArrayBuffer 的型別引數,不過該型別引數具有預設型別實參,以便我們可以繼續引用 Int32Array,而無需顯式寫出 Int32Array<ArrayBufferLike>。
如果在此更新中遇到任何問題,你可能需要更新 @types/node。
這項工作主要由 Kenta Moriuchi 提供!
搜尋上級配置檔案以確定專案所有權
當使用 TSServer(如 Visual Studio 或 VS Code)在編輯器中載入 TypeScript 檔案時,編輯器會嘗試找到“擁有”該檔案的相關 tsconfig.json 檔案。為此,它會從所編輯的檔案開始沿目錄樹向上查詢名為 tsconfig.json 的檔案。
以前,此搜尋會在找到的第一個 tsconfig.json 檔案處停止;但試想如下專案結構:
project/├── src/│ ├── foo.ts│ ├── foo-test.ts│ ├── tsconfig.json│ └── tsconfig.test.json└── tsconfig.json
在這裡,src/tsconfig.json 是專案的“主要”配置檔案,而 src/tsconfig.test.json 是用於執行測試的配置檔案。
json// src/tsconfig.json{"compilerOptions": {"outDir": "../dist"},"exclude": ["**/*.test.ts"]}
json// src/tsconfig.test.json{"compilerOptions": {"outDir": "../dist/test"},"include": ["**/*.test.ts"],"references": [{ "path": "./tsconfig.json" }]}
json// tsconfig.json{// This is a "workspace-style" or "solution-style" tsconfig.// Instead of specifying any files, it just references all the actual projects."files": [],"references": [{ "path": "./src/tsconfig.json" },{ "path": "./src/tsconfig.test.json" },]}
問題在於,當編輯 foo-test.ts 時,編輯器會將 project/src/tsconfig.json 視為“所屬”配置檔案——但這並不是我們想要的!如果查詢在此處停止,可能並不理想。此前避免這種情況的唯一方法是將 src/tsconfig.json 重新命名為 src/tsconfig.src.json 之類,然後所有檔案都會匹配到引用每個可能專案的頂層 tsconfig.json。
project/├── src/│ ├── foo.ts│ ├── foo-test.ts│ ├── tsconfig.src.json│ └── tsconfig.test.json└── tsconfig.json
為了不再強制開發者這樣做,TypeScript 5.7 現在會繼續沿目錄樹向上查詢,以尋找其他合適的 tsconfig.json 檔案以供編輯器場景使用。這可以為專案的組織和配置檔案的結構提供更多靈活性。
在編輯器中對複合專案進行更快的專案所有權檢查
想象一個具有以下結構的大型程式碼庫:
packages├── graphics/│ ├── tsconfig.json│ └── src/│ └── ...├── sound/│ ├── tsconfig.json│ └── src/│ └── ...├── networking/│ ├── tsconfig.json│ └── src/│ └── ...├── input/│ ├── tsconfig.json│ └── src/│ └── ...└── app/├── tsconfig.json├── some-script.js└── src/└── ...
packages 中的每個目錄都是一個獨立的 TypeScript 專案,而 app 目錄是依賴於所有其他專案的主專案。
json// app/tsconfig.json{"compilerOptions": {// ...},"include": ["src"],"references": [{ "path": "../graphics/tsconfig.json" },{ "path": "../sound/tsconfig.json" },{ "path": "../networking/tsconfig.json" },{ "path": "../input/tsconfig.json" }]}
現在注意,我們在 app 目錄中有一個 some-script.js 檔案。當我們在編輯器中開啟 some-script.js 時,TypeScript 語言服務(它也處理 JavaScript 檔案的編輯器體驗!)必須找出該檔案屬於哪個專案,以便應用正確的設定。
在這種情況下,最近的 tsconfig.json 並不包含 some-script.js,但 TypeScript 會繼續詢問“app/tsconfig.json 引用的專案中是否有包含 some-script.js 的專案?”。為了做到這一點,TypeScript 以前會逐個載入每個專案,並在找到包含 some-script.js 的專案後立即停止。即使 some-script.js 未包含在根檔案集中,TypeScript 仍會解析專案內的所有檔案,因為部分根檔案集可能仍然間接引用了 some-script.js。
我們發現,這種行為在大型程式碼庫中會導致極其不穩定和不可預測的表現。開發者在開啟雜亂的指令碼檔案時,會發現自己不得不等待整個程式碼庫被載入。
值得慶幸的是,任何可以被另一個(非工作區)專案引用的專案都必須啟用 composite 標誌,該標誌強制執行所有輸入原始檔必須預先已知的規則。因此,在探測 composite 專案時,TypeScript 5.7 將只檢查檔案是否屬於該專案的根檔案集。這應該能避免這種常見的極端情況。
更多資訊,請參閱此處的更改。
在 --module nodenext 中驗證 JSON 匯入
在 --module nodenext 下從 .json 檔案匯入時,TypeScript 現在將強制執行某些規則以防止執行時錯誤。
首先,對於任何 JSON 檔案匯入,必須存在包含 type: "json" 的匯入屬性。
tsimport myConfig from "./myConfig.json";// ~~~~~~~~~~~~~~~~~// ❌ error: Importing a JSON file into an ECMAScript module requires a 'type: "json"' import attribute when 'module' is set to 'NodeNext'.import myConfig from "./myConfig.json" with { type: "json" };// ^^^^^^^^^^^^^^^^// ✅ This is fine because we provided `type: "json"`
在此驗證之上,TypeScript 將不會生成“命名”匯出,JSON 匯入的內容只能透過預設匯出(default)訪問。
ts// ✅ This is okay:import myConfigA from "./myConfig.json" with { type: "json" };let version = myConfigA.version;///////////import * as myConfigB from "./myConfig.json" with { type: "json" };// ❌ This is not:let version = myConfig.version;// ✅ This is okay:let version = myConfig.default.version;
點選此處瞭解更多有關此更改的資訊。
支援 Node.js 中的 V8 編譯快取
Node.js 22 支援一項名為 module.enableCompileCache() 的新 API。此 API 允許執行時重用工具首次執行後完成的部分解析和編譯工作。
TypeScript 5.7 現在利用了該 API,以便更快地開始執行有意義的工作。在我們自己的某些測試中,我們觀察到執行 tsc --version 的速度提升了約 2.5 倍。
Benchmark 1: node ./built/local/_tsc.js --version (*without* caching)Time (mean ± σ): 122.2 ms ± 1.5 ms [User: 101.7 ms, System: 13.0 ms]Range (min … max): 119.3 ms … 132.3 ms 200 runsBenchmark 2: node ./built/local/tsc.js --version (*with* caching)Time (mean ± σ): 48.4 ms ± 1.0 ms [User: 34.0 ms, System: 11.1 ms]Range (min … max): 45.7 ms … 52.8 ms 200 runsSummarynode ./built/local/tsc.js --version ran2.52 ± 0.06 times faster than node ./built/local/_tsc.js --version
更多資訊,請參閱此處的 Pull Request。
顯著的行為更改
本節重點介紹了一組值得注意的更改,在進行任何升級時都應予以確認和理解。有時它會突出顯示棄用、移除和新的限制。它也可能包含功能上有所改進但透過引入新錯誤而可能影響現有構建的錯誤修復。
lib.d.ts
為 DOM 生成的型別可能會對程式碼庫的型別檢查產生影響。更多資訊,請檢視與此版本 TypeScript 的 DOM 和 lib.d.ts 更新相關的問題。
TypedArray 現在是 ArrayBufferLike 的泛型
在 ECMAScript 2024 中,SharedArrayBuffer 和 ArrayBuffer 的型別略有不同。為了彌合這一差距並保留底層緩衝區型別,所有 TypedArrays(如 Uint8Array 等)現在也變成了泛型。
tsinterface Uint8Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike> {// ...}
每個 TypedArray 現在都包含一個名為 TArrayBuffer 的型別引數,不過該型別引數具有預設型別實參,以便使用者可以繼續引用 Int32Array,而無需顯式寫出 Int32Array<ArrayBufferLike>。
如果在此更新中遇到任何問題,例如:
error TS2322: Type 'Buffer' is not assignable to type 'Uint8Array<ArrayBufferLike>'.error TS2345: Argument of type 'Buffer' is not assignable to parameter of type 'Uint8Array<ArrayBufferLike>'.error TS2345: Argument of type 'ArrayBufferLike' is not assignable to parameter of type 'ArrayBuffer'.error TS2345: Argument of type 'Buffer' is not assignable to parameter of type 'string | ArrayBufferView | Stream | Iterable<string | ArrayBufferView> | AsyncIterable<string | ArrayBufferView>'.
那麼你可能需要更新 @types/node。
從類中的非字面量方法名建立索引簽名
TypeScript 現在對類中以非字面量計算屬性名宣告的方法有了更一致的行為。例如,在以下程式碼中:
tsdeclare const symbolMethodName: symbol;export class A {[symbolMethodName]() { return 1 };}
以前,TypeScript 僅以如下方式看待類:
tsexport class A {}
換句話說,從型別系統的角度來看,[symbolMethodName] 對 A 的型別沒有任何貢獻。
TypeScript 5.7 現在更具意義地看待方法 [symbolMethodName]() {},並生成了一個索引簽名。因此,上述程式碼被解釋為類似於以下程式碼:
tsexport class A {[x: symbol]: () => number;}
這提供了與物件字面量中的屬性和方法一致的行為。
對返回 null 和 undefined 的函式產生更多的隱式 any 錯誤
當函式表示式被返回泛型型別的簽名進行上下文型別化時,TypeScript 現在會在 noImplicitAny 下、但在 strictNullChecks 之外適當地提供隱式 any 錯誤。
tsdeclare var p: Promise<number>;const p2 = p.catch(() => null);// ~~~~~~~~~~// error TS7011: Function expression, which lacks return-type annotation, implicitly has an 'any' return type.