返回表达式中分支的细粒度检查
考虑以下代码:
tsdeclare const untypedCache: Map<any, any>;function getUrlObject(urlString: string): URL {return untypedCache.has(urlString) ?untypedCache.get(urlString) :urlString;}
这段代码的意图是从缓存中检索 URL 对象(如果存在),或者在不存在时创建一个新的 URL 对象。然而,这里有一个错误:我们忘记了根据输入真正构造一个新的 URL 对象。遗憾的是,TypeScript 通常不会捕获这类错误。
当 TypeScript 检查像 cond ? trueBranch : falseBranch 这样的条件表达式时,其类型被视为两个分支类型的联合。换句话说,它获取 trueBranch 和 falseBranch 的类型,并将它们合并为一个联合类型。在本例中,untypedCache.get(urlString) 的类型为 any,而 urlString 的类型为 string。这就是问题所在,因为 any 在与其他类型交互时具有极强的感染力。联合类型 any | string 会被简化为 any,因此当 TypeScript 开始检查 return 语句中的表达式是否与预期的 URL 返回类型兼容时,类型系统已经丢失了本可以捕获该错误的任何信息。
在 TypeScript 5.8 中,类型系统对位于 return 语句内的条件表达式进行了特殊处理。条件表达式的每个分支都会根据包含函数的声明返回类型(如果存在)进行检查,因此类型系统可以捕获上述示例中的错误。
tsdeclare const untypedCache: Map<any, any>;function getUrlObject(urlString: string): URL {return untypedCache.has(urlString) ?untypedCache.get(urlString) :urlString;// ~~~~~~~~~// error! Type 'string' is not assignable to type 'URL'.}
此项更改是在 此 Pull Request 中完成的,作为 TypeScript 未来更广泛改进的一部分。
在 --module nodenext 中支持 require() ECMAScript 模块
多年来,Node.js 一直在 CommonJS 模块之外支持 ECMAScript 模块 (ESM)。遗憾的是,两者之间的互操作性存在一些挑战。
- ESM 文件可以
importCommonJS 文件 - CommonJS 文件 不能
require()ESM 文件
换句话说,从 ESM 文件消费 CommonJS 文件是可行的,反之则不然。这为想要提供 ESM 支持的库作者带来了许多挑战。这些库作者要么必须破坏与 CommonJS 用户的兼容性,要么“双重发布”其库(为 ESM 和 CommonJS 提供独立的入口点),或者干脆无限期地留在 CommonJS 上。虽然双重发布听起来像是一个很好的折中方案,但它是一个复杂且容易出错的过程,还会使包中的代码量大约增加一倍。
Node.js 22 放宽了一些限制,允许从 CommonJS 模块 require("esm") 调用 ECMAScript 模块。Node.js 仍然不允许对包含顶层 await 的 ESM 文件使用 require(),但现在大多数其他 ESM 文件都可以从 CommonJS 文件中进行消费。这为库作者提供了一个无需双重发布即可提供 ESM 支持的重要机会。
TypeScript 5.8 在 --module nodenext 标志下支持此行为。启用 --module nodenext 后,TypeScript 将不会再针对这些 require() ESM 文件的调用发出错误。
由于此功能可能会向后移植到旧版本的 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下禁止require()ECMAScript 模块,但在nodenext下是允许的。 - 导入断言(import assertions,现已废弃,改为支持导入属性 import attributes)在
node18下是允许的,但在nodenext下是不允许的。
详情请参阅 --module node18 的 Pull Request 以及 对 --module nodenext 所做的更改。
--erasableSyntaxOnly 选项
最近,Node.js 23.6 取消了直接运行 TypeScript 文件的实验性支持的标志;然而,此模式仅支持特定的语法结构。Node.js 取消了一个名为 --experimental-strip-types 的模式标志,该模式要求任何 TypeScript 特有的语法都不能具有运行时语义。换句话说,必须能够轻松地从文件中“擦除”或“剥离”任何 TypeScript 特有的语法,并留下一个有效的 JavaScript 文件。
这意味着以下结构是不支持的:
enum声明- 包含运行时代码的
namespace和module - 类中的参数属性 (parameter properties)
- 非 ECMAScript 的
import =和export =赋值
以下是一些无效示例:
ts// ❌ error: An `import ... = require(...)` aliasimport foo = require("foo");// ❌ error: A namespace with runtime code.namespace container {}// ❌ error: An `import =` aliasimport Bar = container.Bar;class Point {// ❌ error: Parameter propertiesconstructor(public x: number, public y: number) { }}// ❌ error: An `export =` assignment.export = Point;// ❌ error: An enum declaration.enum Direction {Up,Down,Left,Right,}
类似的工具,如 ts-blank-space 或 Amaro(Node.js 中类型剥离的底层库)具有相同的局限性。如果这些工具遇到不符合要求的代码,它们会提供有用的错误信息,但你仍然只有在尝试运行代码时才会发现它无法工作。
这就是为什么 TypeScript 5.8 引入了 --erasableSyntaxOnly 标志的原因。启用此标志后,TypeScript 将对大多数具有运行时行为的 TypeScript 特有结构报错。
tsclass C {constructor(public x: number) { }// ~~~~~~~~~~~~~~~~// error! This syntax is not allowed when 'erasableSyntaxOnly' is enabled.}}
通常情况下,你会希望将此标志与 --verbatimModuleSyntax 结合使用,后者可确保模块包含适当的导入语法,并且不会发生导入省略。
更多信息,请参阅此处的实现。
--libReplacement 标志
在 TypeScript 4.5 中,我们引入了用自定义文件替换默认 lib 文件的可能性。这是基于从名为 @typescript/lib-* 的包中解析库文件的能力。例如,你可以通过以下 package.json 将你的 dom 库锁定到特定版本的 @types/web 包。
json{"devDependencies": {"@typescript/lib-dom": "npm:@types/web@0.0.199"}}
安装后,应该存在一个名为 @typescript/lib-dom 的包,目前 TypeScript 在设置中隐含 dom 时总是会查找它。
这是一个强大的功能,但也会带来一些额外的工作。即使你不使用此功能,TypeScript 也会始终执行此查找,并且必须监视 node_modules 中的变化,以防 lib 替换包开始出现。
TypeScript 5.8 引入了 --libReplacement 标志,允许你禁用此行为。如果你不使用 --libReplacement,现在可以通过 --libReplacement false 禁用它。将来 --libReplacement false 可能会成为默认设置,因此如果你目前依赖此行为,应考虑通过 --libReplacement true 显式启用它。
更多信息,请参阅此处的更改。
声明文件中保留的计算属性名
为了使计算属性在声明文件中的输出更具可预测性,TypeScript 5.8 将始终在类中的计算属性名中保留实体名称(bareVariables 和 dotted.names.that.look.like.this)。
例如,考虑以下代码:
tsexport let propName = "theAnswer";export class MyClass {[propName] = 42;// ~~~~~~~~~~// error!// A computed property name in a class property declaration must have a simple literal type or a 'unique symbol' type.}
以前版本的 TypeScript 在为此模块生成声明文件时会报错,生成的最佳努力声明文件会产生一个索引签名。
tsexport declare let propName: string;export declare class MyClass {[x: string]: number;}
在 TypeScript 5.8 中,上述示例代码现在被允许,且生成的声明文件将与你编写的代码相匹配:
tsexport 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 代码”)。它们后来被重新设计为一个名为 导入属性 (import attributes) 的提案。作为转型的一部分,它们将使用的 assert 关键字改为了 with 关键字。
ts// An import assertion ❌ - not future-compatible with most runtimes.import data from "./data.json" assert { type: "json" };// An import attribute ✅ - the preferred way to import a JSON file.import data from "./data.json" with { type: "json" };
Node.js 22 不再接受使用 assert 语法的导入断言。因此,当在 TypeScript 5.8 中启用 --module nodenext 时,如果遇到导入断言,TypeScript 将发出错误。
tsimport data from "./data.json" assert { type: "json" };// ~~~~~~// error! Import assertions have been replaced by import attributes. Use 'with' instead of 'assert'
更多信息,请参阅此处的更改。