Skip to content
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待

TypeScript 5.8

return 表达式中分支的精细检查

考虑如下代码:

ts
declare const untypedCache: Map<any, any>;

function getUrlObject(urlString: string): URL {
    return untypedCache.has(urlString) ?
        untypedCache.get(urlString) :
        urlString;
}

这段代码的意图是:如果缓存中存在 URL 对象则返回它,否则创建一个新的 URL 对象。然而,这里有一个 bug:我们忘记了实际用输入来构造一个新的 URL 对象。不幸的是,TypeScript 通常不会捕捉到这类错误。

当 TypeScript 检查条件表达式(如 cond ? trueBranch : falseBranch)时,其类型被处理为两个分支类型的联合。换句话说,它获取 trueBranchfalseBranch 的类型,并将它们组合成一个联合类型。在这个例子中,untypedCache.get(urlString) 的类型是 any,而 urlString 的类型是 string。问题就出在这里,因为 any 在与其他类型交互时具有很强的传染性。联合 any | string 被简化为 any,因此当 TypeScript 开始检查 return 语句中的表达式是否与期望的返回类型 URL 兼容时,类型系统已经丢失了任何能捕捉到代码中这个 bug 的信息。

在 TypeScript 5.8 中,类型系统对直接位于 return 语句内部的条件表达式进行了特殊处理。条件表达式的每个分支都会根据包含函数的声明返回类型(如果存在)进行检查,因此类型系统能够捕捉到上面示例中的 bug。

ts
declare const untypedCache: Map<any, any>;

function getUrlObject(urlString: string): URL {
    return untypedCache.has(urlString) ?
        untypedCache.get(urlString) :
        urlString;
    //  ~~~~~~~~~
    // 错误!类型 'string' 不能赋值给类型 'URL'。
}

此项更改在此拉取请求中实现,是 TypeScript 未来一系列改进的一部分。

--module nodenext 中支持对 ECMAScript 模块的 require()

多年来,Node.js 在支持 CommonJS 模块的同时也支持 ECMAScript 模块 (ESM)。不幸的是,两者之间的互操作性存在一些挑战。

  • ESM 文件可以 import CommonJS 文件
  • CommonJS 文件无法 require() ESM 文件

换句话说,从 ESM 文件消费 CommonJS 文件是可行的,但反过来则不行。这给希望提供 ESM 支持的库作者带来了许多挑战。这些库作者要么必须打破与 CommonJS 用户的兼容性,要么“双发布”他们的库(为 ESM 和 CommonJS 提供单独的入口点),要么无限期地停留在 CommonJS 上。虽然双发布听起来像是一个不错的折中方案,但这是一个复杂且容易出错的过程,并且大致会使包内的代码量翻倍。

Node.js 22 放宽了这些限制中的一部分,允许从 CommonJS 模块对 ECMAScript 模块进行 require("esm") 调用。Node.js 仍然不允许对包含顶层 await 的 ESM 文件使用 require(),但大多数其他 ESM 文件现在可以从 CommonJS 文件中消费。这为库作者提供了一个重大机遇,可以在无需双发布其库的情况下提供 ESM 支持。

TypeScript 5.8 在 --module nodenext 标志下支持此行为。当启用 --module nodenext 时,TypeScript 将避免对这些对 ESM 文件的 require() 调用报错。

由于此功能可能会被移植到较旧版本的 Node.js,因此目前还没有稳定的 --module nodeXXXX 选项来启用此行为;然而,我们预计未来的 TypeScript 版本可能能够在 node20 下稳定此功能。在此期间,我们鼓励 Node.js 22 及更新版本的用户使用 --module nodenext,而库作者和较旧 Node.js 版本的用户应继续使用 --module node16(或进行小幅更新到 --module node18)。

有关更多信息,请在此处查看我们对 require("esm") 的支持

--module node18

TypeScript 5.8 引入了稳定的 --module node18 标志。对于固定在 Node.js 18 上的用户,此标志提供了一个稳定的参考点,不包含 --module nodenext 中的某些行为。具体来说:

  • node18 下不允许对 ECMAScript 模块进行 require(),但在 nodenext 下允许
  • node18 下允许导入断言(已被导入属性取代),但在 nodenext 下不允许

更多详情请参阅 --module node18 拉取请求--module nodenext 的更改

--erasableSyntaxOnly 选项

最近,Node.js 23.6 取消了直接运行 TypeScript 文件的实验性支持的标志;然而,在此模式下仅支持某些结构。Node.js 取消了一个名为 --experimental-strip-types 的模式,该模式要求任何 TypeScript 特定的语法不能具有运行时语义。换句话说,必须能够轻松地从文件中擦除或“剥离”任何 TypeScript 特定语法,留下一个有效的 JavaScript 文件。

这意味着不支持以下结构:

  • enum 声明
  • 包含运行时代码的 namespacemodule
  • 类中的参数属性
  • 非 ECMAScript 的 import =export = 赋值

以下是一些不起作用的示例:

ts
// ❌ 错误:一个 `import ... = require(...)` 别名
import foo = require("foo");

// ❌ 错误:一个包含运行时代码的命名空间。
namespace container {
}

// ❌ 错误:一个 `import =` 别名
import Bar = container.Bar;

class Point {
    // ❌ 错误:参数属性
    constructor(public x: number, public y: number) { }
}

// ❌ 错误:一个 `export =` 赋值。
export = Point;

// ❌ 错误:一个 enum 声明。
enum Direction {
    Up,
    Down,
    Left,
    Right,
}

类似的工具如 ts-blank-spaceAmaro(Node.js 中类型剥离的底层库)也有相同的限制。如果遇到不符合这些要求的代码,这些工具会提供有用的错误消息,但你仍然只有在你实际尝试运行代码时才会发现代码不起作用。

这就是为什么 TypeScript 5.8 引入了 --erasableSyntaxOnly 标志。启用此标志后,TypeScript 将对大多数具有运行时行为的 TypeScript 特定结构报错。

ts
class C {
    constructor(public x: number) { }
    //          ~~~~~~~~~~~~~~~~
    // 错误!当启用 'erasableSyntaxOnly' 时,不允许使用此语法。
    }
}

通常,你会希望将此标志与 --verbatimModuleSyntax 结合使用,后者确保模块包含适当的导入语法,并且不会发生导入省略。

有关更多信息,请在此处查看实现

--libReplacement 标志

在 TypeScript 4.5 中,我们引入了用自定义文件替换默认 lib 文件的可能性。这基于从名为 @typescript/lib-* 的包中解析库文件的可能性。例如,你可以通过以下 package.jsondom 库锁定到 @types/web 的特定版本:

json
{
    "devDependencies": {
       "@typescript/lib-dom": "npm:@types/web@0.0.199"
     }
}

安装后,应该存在一个名为 @typescript/lib-dom 的包,当你的设置隐含 dom 时,TypeScript 当前将始终查找它。

这是一个强大的功能,但它也会带来一些额外的工作。即使你不使用此功能,TypeScript 也始终执行此查找,并且必须监视 node_modules 中的更改,以防 lib 替换包开始存在。

TypeScript 5.8 引入了 --libReplacement 标志,允许你禁用此行为。如果你没有使用 --libReplacement,现在可以通过 --libReplacement false 禁用它。将来 --libReplacement false 可能会成为默认值,因此如果你当前依赖此行为,应考虑通过 --libReplacement true 显式启用它。

有关更多信息,请在此处查看更改

声明文件中保留的计算属性名称

为了使计算属性在声明文件中具有更可预测的输出,TypeScript 5.8 将一致地在类的计算属性名称中保留实体名称(bareVariablesdotted.names.that.look.like.this)。

例如,考虑以下代码:

ts
export let propName = "theAnswer";

export class MyClass {
    [propName] = 42;
//  ~~~~~~~~~~
// 错误!
// 类属性声明中的计算属性名称必须具有简单的字面量类型或 'unique symbol' 类型。
}

以前版本的 TypeScript 在为该模块生成声明文件时会报错,并且会尽力生成一个包含索引签名的声明文件。

ts
export declare let propName: string;
export declare class MyClass {
    [x: string]: number;
}

在 TypeScript 5.8 中,示例代码现在被允许,并且生成的声明文件将与你编写的内容匹配:

ts
export declare let propName: string;
export declare class MyClass {
    [propName]: number;
}

请注意,这不会在类上创建静态命名的属性。你最终得到的仍然是一个有效的索引签名,如 [x: string]: number,因此对于该用例,你需要使用 unique symbol 或字面量类型。

请注意,在 --isolatedDeclarations 标志下,编写此代码过去是且现在仍然是错误;但我们期望由于此项更改,计算属性名称通常将被允许出现在声明输出中。

需要注意的是,在 TypeScript 5.8 中编译的文件可能会生成一个在 TypeScript 5.7 或更早版本中不向后兼容的声明文件(尽管可能性很小)。

有关更多信息,请参阅实现 PR

程序加载和更新的优化

TypeScript 5.8 引入了一系列优化,既可以改善构建程序的时间,也可以改善在 --watch 模式或编辑器场景下基于文件更改更新程序的时间。

首先,TypeScript 现在避免在规范化路径时涉及的数组分配。通常,路径规范化会将路径的每个部分分割成一个字符串数组,根据相对段规范化结果路径,然后使用规范分隔符将它们重新连接起来。对于具有许多文件的项目,这可能是一项重要且重复的工作。TypeScript 现在避免了分配数组,而是更直接地在原始路径的索引上操作。

此外,当进行不改变项目基本结构的编辑时,TypeScript 现在避免重新验证提供给它的选项(例如 tsconfig.json 的内容)。这意味着,例如,一个简单的编辑可能不需要检查项目的输出路径是否与输入路径冲突。相反,可以使用上次检查的结果。这应该会使大型项目中的编辑响应更快。

值得注意的行为变化

本节重点介绍一组在升级时应该注意和理解的重要更改。有时它会突出显示弃用、删除和新限制。它还可能包含功能上改进的错误修复,但也可能通过引入新错误来影响现有构建。

lib.d.ts

为 DOM 生成的类型可能会对代码库的类型检查产生影响。有关更多信息,请查看与此版本 TypeScript 的 DOM 和 lib.d.ts 更新相关的链接问题

--module nodenext 下对导入断言(Import Assertions)的限制

导入断言是 ECMAScript 的一个提案,用于确保导入的某些属性(例如“此模块是 JSON,不是旨在执行的可执行 JavaScript 代码”)。它们被重新发明为称为导入属性的提案。作为过渡的一部分,它们从使用 assert 关键字切换为使用 with 关键字。

ts
// 导入断言 ❌ - 与大多数运行时不兼容的未来。
import data from "./data.json" assert { type: "json" };

// 导入属性 ✅ - 导入 JSON 文件的首选方式。
import data from "./data.json" with { type: "json" };

Node.js 22 不再接受使用 assert 语法的导入断言。因此,当在 TypeScript 5.8 中启用 --module nodenext 时,如果遇到导入断言,TypeScript 将报错。

ts
import data from "./data.json" assert { type: "json" };
//                             ~~~~~~
// 错误!导入断言已被导入属性取代。请使用 'with' 代替 'assert'

有关更多信息,请在此处查看更改