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 中,这些选项和行为将变成严格的错误,使用它们的代码将需要进行迁移以避免编译错误。因此,建议开发人员尽早迁移其代码库,以避免未来兼容性问题。