TypeScript 5.4 正式發(fā)布,一起來看看該版本帶來了那些更新
3 月 6 日,TypeScript 發(fā)布了 v5.4 版本,該版本帶來了以下更新:
- 類型縮小會(huì)在閉包中保留
- 引入新的實(shí)用程序類型 NoInfer<T>
- 新增Object.groupBy 和 Map.groupBy
- 新的模塊解析選項(xiàng)
- 新的模塊導(dǎo)入檢查機(jī)制
- TypeScript 5.5 即將棄用的功能
類型縮小會(huì)在閉包中保留
TypeScript 通過類型縮小來優(yōu)化代碼,但在閉包中并不總是保留這些縮小后的類型。從TypeScript 5.4開始,當(dāng)在非提升函數(shù)中使用參數(shù)或let變量時(shí),類型檢查器會(huì)查找最后的賦值點(diǎn),從而智能地進(jìn)行類型縮小。然而,如果變量在嵌套函數(shù)中被重新分配,即使這種分配不影響其類型,也會(huì)使閉包中的類型細(xì)化無效。
// TypeScript的類型縮小在閉包中通常不保留
function exampleFunction(input: string | number) {
if (typeof input === "string") {
input = parseInt(input); // 假設(shè)想要將字符串轉(zhuǎn)為數(shù)字
}
return () => {
// 在這里,TypeScript不知道input是string還是number
// 因?yàn)樵陂]包創(chuàng)建后,input可能已經(jīng)被修改
console.log(input.toString()); // 錯(cuò)誤!'input'可能是number,沒有toString方法
};
}
TypeScript 5.4后,當(dāng)在閉包外部對(duì)變量進(jìn)行最后一次賦值時(shí),類型縮小會(huì)在閉包中保留:
function improvedFunction(input: string | number) {
let value;
if (typeof input === "string") {
value = parseInt(input);
} else {
value = input;
}
return () => {
// 在這里,TypeScript知道value是number,因?yàn)檫@是在閉包創(chuàng)建后的最后一次賦值
console.log(value.toString()); // 正確!因?yàn)楝F(xiàn)在我們知道value是number
};
}
引入新的實(shí)用程序類型 NoInfer
TypeScript的泛型函數(shù)能夠根據(jù)傳入的參數(shù)自動(dòng)推斷類型。但在某些情況下,這種自動(dòng)推斷可能不符合預(yù)期,導(dǎo)致不合法的函數(shù)調(diào)用被接受,而合法的調(diào)用卻被拒絕。為了處理這種情況,開發(fā)者通常需要添加額外的類型參數(shù)來約束函數(shù)的行為,確保類型安全。但這種做法可能會(huì)使代碼看起來更加復(fù)雜,特別是當(dāng)這些額外的類型參數(shù)在函數(shù)簽名中只使用一次時(shí)。
TypeScript 5.4 引入了 NoInfer<T> 實(shí)用類型,允許開發(fā)者明確告訴編譯器哪些類型不應(yīng)該被自動(dòng)推斷。這避免了不合法的函數(shù)調(diào)用被接受,增強(qiáng)了代碼的類型安全性。
考慮以下函數(shù),它接受一個(gè)用戶ID列表和一個(gè)可選的默認(rèn)用戶ID。
function selectUser<U extends string>(userIds: U[], defaultUserId?: U) {
// ...
}
const userIds = ["123", "456", "789"];
selectUser(userIds, "000"); // 錯(cuò)誤地被接受,因?yàn)?000"不在userIds中
在這個(gè)例子中,即使"000"不在userIds數(shù)組中,selectUser函數(shù)的調(diào)用也會(huì)被接受,因?yàn)門ypeScript自動(dòng)推斷默認(rèn)用戶ID可以是任何字符串。
TypeScript 5.4 中:
function selectUser<U extends string>(userIds: U[], defaultUserId?: NoInfer<U>) {
// ...
}
const userIds = ["123", "456", "789"];
selectUser(userIds, "000"); // 正確的錯(cuò)誤,因?yàn)?000"不在userIds中
通過使用 NoInfer<T> 告訴 TypeScript 不要推斷默認(rèn)用戶ID的類型,從而確保只有當(dāng)默認(rèn)用戶ID在userIds數(shù)組中時(shí)才接受調(diào)用。這增強(qiáng)了代碼的類型安全性,避免了潛在的錯(cuò)誤。
新增 Object.groupBy 和 Map.groupBy
TypeScript 5.4 引入了兩個(gè)新方法:Object.groupBy 和 Map.groupBy,它們用于根據(jù)特定條件將數(shù)組元素分組。
- Object.groupBy 返回一個(gè)對(duì)象,其中每個(gè)鍵代表一個(gè)分組,對(duì)應(yīng)的值是該分組的元素?cái)?shù)組。
- Map.groupBy 返回一個(gè)`` Map 對(duì)象,實(shí)現(xiàn)了相同的功能,但允許使用任何類型的鍵。
使用 Object.groupBy 和 Map.groupBy 可以方便地根據(jù)自定義邏輯對(duì)數(shù)組進(jìn)行分組,無需手動(dòng)創(chuàng)建和填充對(duì)象或 Map。然而,在使用 Object.groupBy 時(shí),由于對(duì)象的屬性名必須是有效的標(biāo)識(shí)符,因此可能無法覆蓋所有情況。此外,這些方法目前僅在 esnext 目標(biāo)或特定庫設(shè)置下可用。
假設(shè)有一個(gè)學(xué)生數(shù)組,每個(gè)學(xué)生都有姓名和成績。我們想要根據(jù)成績將學(xué)生分為“優(yōu)秀”和“及格”兩組。
const students: { name: string, score: number }[] = [
{ name: "Alice", score: 90 },
{ name: "Bob", score: 75 },
{ name: "Charlie", score: 85 },
// ...其他學(xué)生
];
const groupedStudents: { excellent: any[], passing: any[] } = {
excellent: [],
passing: []
};
for (const student of students) {
if (student.score >= 80) {
groupedStudents.excellent.push(student);
} else {
groupedStudents.passing.push(student);
}
}
使用 Array.prototype.groupBy 方法,可以更簡潔地實(shí)現(xiàn)相同的功能。
const students: { name: string, score: number }[] = [
{ name: "Alice", score: 90 },
{ name: "Bob", score: 75 },
{ name: "Charlie", score: 85 },
// ...其他學(xué)生
];
const groupedStudents = students.groupBy(student => {
return student.score >= 80 ? "excellent" : "passing";
});
// 使用時(shí)可以直接訪問分組
console.log(groupedStudents.get("excellent")); // 輸出優(yōu)秀學(xué)生數(shù)組
console.log(groupedStudents.get("passing")); // 輸出及格學(xué)生數(shù)組
在這個(gè)例子中,groupBy 方法根據(jù)每個(gè)學(xué)生的成績將學(xué)生數(shù)組分為“優(yōu)秀”和“及格”兩組,并返回一個(gè) Map 對(duì)象,其中鍵是分組名稱,值是對(duì)應(yīng)的學(xué)生數(shù)組。這種方法更加簡潔且易于理解。
新的模塊解析選項(xiàng)
TypeScript 5.4 引入了一個(gè)新的模塊解析選項(xiàng) bundler,它模擬了現(xiàn)代構(gòu)建工具(如Webpack、Vite 等)確定導(dǎo)入路徑的方式。當(dāng)與 --module esnext 配合使用時(shí),它允許開發(fā)者使用標(biāo)準(zhǔn)的 ECMAScript 導(dǎo)入語法,但禁止了 import ... = require(...) 這種混合語法。
同時(shí),TypeScript 5.4 還增加了一個(gè)名為 preserve 的模塊選項(xiàng),該選項(xiàng)允許開發(fā)者在 TypeScript 中使用 require(),并更準(zhǔn)確地模擬了構(gòu)建工具和其他運(yùn)行時(shí)環(huán)境的模塊查找行為。當(dāng)設(shè)置 module 為 preserve 時(shí),構(gòu)建工具會(huì)隱式地成為默認(rèn)的模塊解析策略,同時(shí)啟用 esModuleInterop 和 resolveJsonModule。
假設(shè)有一個(gè)使用 TypeScript 編寫的項(xiàng)目,并且想從一個(gè)名為 my-lib 的庫中導(dǎo)入兩個(gè)模塊 moduleA 和 moduleB。這個(gè)庫提供了 ES 模塊和 CommonJS 模塊兩種格式。在 TypeScript 配置中,你可能這樣設(shè)置:
// tsconfig.json
{
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node"
}
}
然后代碼中這樣導(dǎo)入:
import * as moduleA from 'my-lib/moduleA';
import * as moduleB = require('my-lib/moduleB');
在這種情況下,TypeScript 可能會(huì)為兩個(gè)導(dǎo)入生成相同的路徑,因?yàn)樗鼈兌际褂昧?Node.js 的模塊解析策略。
在 TypeScript 5.4 中,如果想更精確地控制導(dǎo)入的路徑,特別是當(dāng)庫提供了基于導(dǎo)入語法的不同實(shí)現(xiàn)時(shí),可以使用 preserve
模塊選項(xiàng)和構(gòu)建工具模塊解析策略:
// tsconfig.json
{
"compilerOptions": {
"module": "preserve",
// 隱式設(shè)置:
// "moduleResolution": "bundler",
// "esModuleInterop": true,
// "resolveJsonModule": true
}
}
然后,可以這樣編寫代碼:
import * as moduleA from 'my-lib/moduleA'; // 使用 ES 模塊導(dǎo)入
const moduleB = require('my-lib/moduleB'); // 使用 CommonJS 模塊導(dǎo)入
現(xiàn)在,TypeScript 會(huì)根據(jù) my-lib 的 package.json 文件中的 exports 字段來決定使用哪個(gè)文件路徑。如果庫為 ES 模塊和 CommonJS 模塊提供了不同的文件,TypeScript 將根據(jù)導(dǎo)入的語法(import 或 require)選擇正確的文件。
這意味著開發(fā)者可以更精細(xì)地控制模塊導(dǎo)入的行為,確保與庫的意圖一致,尤其是在處理那些提供條件導(dǎo)出的庫時(shí)。
新的模塊導(dǎo)入檢查機(jī)制
TypeScript 5.4 引入了新的模塊導(dǎo)入檢查機(jī)制,確保導(dǎo)入的屬性與全局定義的 ImportAttributes 接口相匹配。這種檢查提高了代碼的準(zhǔn)確性,因?yàn)槿魏尾环显摻涌诘膶?dǎo)入屬性都會(huì)導(dǎo)致編譯錯(cuò)誤。
在早期的 TypeScript 版本中,開發(fā)者可以自由地為 import
語句指定任何導(dǎo)入屬性,而不會(huì)有嚴(yán)格的類型檢查。這可能導(dǎo)致運(yùn)行時(shí)錯(cuò)誤,因?yàn)閷?dǎo)入的屬性可能與實(shí)際的模塊不匹配。
// 假設(shè)存在一個(gè)全局的模塊定義,但沒有明確的導(dǎo)入屬性類型
import * as myModule from 'my-module' with { custom: 'value' };
在上述代碼中,custom 屬性是自由定義的,沒有與任何全局接口或類型進(jìn)行匹配,這增加了出錯(cuò)的風(fēng)險(xiǎn)。
在 TypeScript 5.4 及以后的版本中,開發(fā)者必須確保導(dǎo)入屬性與全局定義的 ImportAttributes 接口相符。這確保了類型安全,并減少了潛在的運(yùn)行時(shí)錯(cuò)誤。
// 全局定義的導(dǎo)入屬性接口
interface ImportAttributes {
validProperty: string;
}
// 在模塊中導(dǎo)入時(shí),必須使用符合 ImportAttributes 接口的屬性
import * as myModule from 'my-module' with { validProperty: 'someValue' };
// 下面的導(dǎo)入將引發(fā)錯(cuò)誤,因?yàn)閷傩悦Q不匹配
import * as myModule from 'my-module' with { invalidProperty: 'someValue' };
// 錯(cuò)誤:屬性 'invalidProperty' 不存在于類型 'ImportAttributes' 中
在這個(gè)新版本中,如果開發(fā)者嘗試使用不符合 ImportAttributes 接口的導(dǎo)入屬性,TypeScript 編譯器將拋出錯(cuò)誤,從而避免了潛在的錯(cuò)誤。
TypeScript 5.5 即將棄用的功能
TypeScript 5.0 已經(jīng)廢棄了以下選項(xiàng)和行為:
- charset
- target: ES3
- importsNotUsedAsValues
- noImplicitUseStrict
- noStrictGenericChecks
- keyofStringsOnly
- suppressExcessPropertyErrors
- suppressImplicitAnyIndexErrors
- out
- preserveValueImports
- 在項(xiàng)目引用中的prepend
- 隱式OS特定的newLine
為了在 TypeScript 5.0 及更高版本中繼續(xù)使用這些已廢棄的選項(xiàng)和行為,開發(fā)人員必須指定一個(gè)新的選項(xiàng) ignoreDeprecations,并將其值設(shè)置為 "5.0"。
注意,TypeScript 5.4 將是這些已廢棄選項(xiàng)和行為按預(yù)期運(yùn)作的最后一個(gè)版本。在預(yù)計(jì)于 2024 年 6 月發(fā)布的 TypeScript 5.5 中,這些選項(xiàng)和行為將變成嚴(yán)格的錯(cuò)誤,使用它們的代碼將需要進(jìn)行遷移以避免編譯錯(cuò)誤。因此,建議開發(fā)人員盡早遷移其代碼庫,以避免未來兼容性問題。