3 月 6 日,TypeScript 釋出了 v5.4 版本,該版本帶來了以下更新:
型別縮小會在閉包中保留
引入新的實用程式型別
NoInfer<T>
新增
Object.groupBy
和
Map.groupBy
新的模組解析選項
新的模組匯入檢查機制
TypeScript 5.5 即將棄用的功能
型別縮小會在閉包中保留
TypeScript 透過型別縮小來最佳化程式碼,但在閉包中並不總是保留這些縮小後的型別。從TypeScript 5.4開始,當在非提升函式中使用參數或
let
變量時,型別檢查器會尋找最後的賦值點,從而智慧地進行型別縮小。然而,如果變量在巢狀函式中被重新分配,即使這種分配不影響其型別,也會使閉包中的型別細化無效。
// TypeScript的型別縮小在閉包中通常不保留
functionexampleFunction(input: string | number){
if(typeof input ==="string"){
input =parseInt(input);// 假設想要將字串轉為數位
}
return()=>{
// 在這裏,TypeScript不知道input是string還是number
// 因為在閉包建立後,input可能已經被修改
console.log(input.toString());// 錯誤!'input'可能是number,沒有toString方法
};
}
TypeScript 5.4後,當在閉包外部對變量進行最後一次賦值時,型別縮小會在閉包中保留:
functionimprovedFunction(input: string | number){
let value;
if(typeof input ==="string"){
value =parseInt(input);
}else{
value = input;
}
return()=>{
// 在這裏,TypeScript知道value是number,因為這是在閉包建立後的最後一次賦值
console.log(value.toString());// 正確!因為現在我們知道value是number
};
}
引入新的實用程式型別 NoInfer
TypeScript的泛型函式能夠根據傳入的參數自動推斷型別。但在某些情況下,這種自動推斷可能不符合預期,導致不合法的函式呼叫被接受,而合法的呼叫卻被拒絕。為了處理這種情況,開發者通常需要添加額外的型別參數來約束函式的行為,確保型別安全。但這種做法可能會使程式碼看起來更加復雜,特別是當這些額外的型別參數在函式簽名中只使用一次時。
TypeScript 5.4 引入了
NoInfer<T>
實用型別,允許開發者明確告訴編譯器哪些型別不應該被自動推斷。這避免了不合法的函式呼叫被接受,增強了程式碼的型別安全性。
考慮以下函式,它接受一個使用者ID列表和一個可選的預設使用者ID。
functionselectUser<Uextendsstring>(userIds:U[], defaultUserId?:U){
// ...
}
const userIds =["123","456","789"];
selectUser(userIds,"000");// 錯誤地被接受,因為"000"不在userIds中
在這個例子中,即使"000"不在userIds陣列中,selectUser函式的呼叫也會被接受,因為TypeScript自動推斷預設使用者ID可以是任何字串。
TypeScript 5.4 中:
functionselectUser<Uextendsstring>(userIds:U[], defaultUserId?: NoInfer<U>){
// ...
}
const userIds =["123","456","789"];
selectUser(userIds,"000");// 正確的錯誤,因為"000"不在userIds中
透過使用
NoInfer<T>
告訴 TypeScript 不要推斷預設使用者ID的型別,從而確保只有當預設使用者ID在
userIds
陣列中時才接受呼叫。這增強了程式碼的型別安全性,避免了潛在的錯誤。
新增 Object.groupBy 和 Map.groupBy
TypeScript 5.4 引入了兩個新方法:
Object.groupBy
和
Map.groupBy
,它們用於根據特定條件將陣列元素分組。
Object.groupBy
返回一個物件,其中每個鍵代表一個分組,對應的值是該分組的元素陣列。
Map.groupBy
返回一個`` Map 物件,實作了相同的功能,但允許使用任何型別的鍵。
使用
Object.groupBy
和
Map.groupBy
可以方便地根據自訂邏輯對陣列進行分組,無需手動建立和填充物件或 Map。然而,在使用
Object.groupBy
時,由於物件的內容名必須是有效的識別元,因此可能無法覆蓋所有情況。此外,這些方法目前僅在 esnext 目標或特定庫設定下可用。
假設有一個學生陣列,每個學生都有姓名和成績。我們想要根據成績將學生分為「優秀」和「及格」兩組。
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
方法,可以更簡潔地實作相同的功能。
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"));// 輸出優秀學生陣列
console.log(groupedStudents.get("passing"));// 輸出及格學生陣列
在這個例子中,groupBy 方法根據每個學生的成績將學生陣列分為「優秀」和「及格」兩組,並返回一個 Map 物件,其中鍵是分組名稱,值是對應的學生陣列。這種方法更加簡潔且易於理解。
新的模組解析選項
TypeScript 5.4 引入了一個新的模組解析選項 bundler,它模擬了現代構建工具(如Webpack、Vite 等)確定匯入路徑的方式。當與
--module esnext
配合使用時,它允許開發者使用標準的 ECMAScript 匯入語法,但禁止了
import ... = require(...)
這種混合語法。
同時,TypeScript 5.4 還增加了一個名為
preserve
的模組選項,該選項允許開發者在 TypeScript 中使用
require()
,並更準確地模擬了構建工具和其他執行時環境的模組尋找行為。當設定
module
為
preserve
時,構建工具會隱式地成為預設的模組解析策略,同時啟用
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 中,如果想更精確地控制匯入的路徑,特別是當庫提供了基於匯入語法的不同實作時,可以使用
preserve
模組選項和構建工具模組解析策略:
// 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 模組匯入
現在,TypeScript 會根據 my-lib 的 package.json 檔中的 exports 欄位來決定使用哪個檔路徑。如果庫為 ES 模組和 CommonJS 模組提供了不同的檔,TypeScript 將根據匯入的語法(
import
或
require
)選擇正確的檔。
這意味著開發者可以更精細地控制模組匯入的行為,確保與庫的意圖一致,尤其是在處理那些提供條件匯出的庫時。
新的模組匯入檢查機制
TypeScript 5.4 引入了新的模組匯入檢查機制,確保匯入的內容與全域定義的 ImportAttributes 介面相匹配。這種檢查提高了程式碼的準確性,因為任何不符合該介面的匯入內容都會導致編譯錯誤。
在早期的 TypeScript 版本中,開發者可以自由地為
import
語句指定任何匯入內容,而不會有嚴格的型別檢查。這可能導致執行時錯誤,因為匯入的內容可能與實際的模組不匹配。
// 假設存在一個全域的模組定義,但沒有明確的匯入內容型別
import*as myModule from'my-module'with{ custom:'value'};
在上述程式碼中,
custom
內容是自由定義的,沒有與任何全域介面或型別進行匹配,這增加了出錯的風險。
在 TypeScript 5.4 及以後的版本中,開發者必須確保匯入內容與全域定義的
ImportAttributes
介面相符。這確保了型別安全,並減少了潛在的執行時錯誤。
// 全域定義的匯入內容介面
interfaceImportAttributes{
validProperty:string;
}
// 在模組中匯入時,必須使用符合 ImportAttributes 介面的內容
import*as myModule from'my-module'with{ validProperty:'someValue'};
// 下面的匯入將引發錯誤,因為內容名稱不匹配
import*as myModule from'my-module'with{ invalidProperty:'someValue'};
// 錯誤:內容 'invalidProperty' 不存在於型別 'ImportAttributes' 中
在這個新版本中,如果開發者嘗試使用不符合
ImportAttributes
介面的匯入內容,TypeScript 編譯器將丟擲錯誤,從而避免了潛在的錯誤。
TypeScript 5.5 即將棄用的功能
TypeScript 5.0 已經廢棄了以下選項和行為:
charset
target: ES3
importsNotUsedAsValues
noImplicitUseStrict
noStrictGenericChecks
keyofStringsOnly
suppressExcessPropertyErrors
suppressImplicitAnyIndexErrors
out
preserveValueImports
在計畫參照中的
prepend
隱式OS特定的
newLine
為了在 TypeScript 5.0 及更高版本中繼續使用這些已廢棄的選項和行為,開發人員必須指定一個新的選項
ignoreDeprecations
,並將其值設定為 "5.0"。
註意,TypeScript 5.4 將是這些已廢棄選項和行為按預期運作的最後一個版本。在預計於 2024 年 6 月釋出的 TypeScript 5.5 中,這些選項和行為將變成嚴格的錯誤,使用它們的程式碼將需要進行遷移以避免編譯錯誤。因此,建議開發人員盡早遷移其程式碼庫,以避免未來相容性問題。