当前位置: 欣欣网 > 码农

TypeScript 5.4 正式发布

2024-03-10码农

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

    往期推荐