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