TS 爱好者请查收:TS 5.5 公测版官宣!(下)
给前端以福利,给编程以复利。大家好,我是大家的林语冰。
00. 观前须知
今年三月份 TS 团队刚刚官宣了 2024 的第一个次版本 TS 5.4(稳定版),仅一个半月后,TS 团队再次升级:TS 5.5 Beta(公测版)发布!本期一起来预习一下 TS 5.5 的官方博客。
免责声明
本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 Announcing TypeScript 5.5 Beta。
01. ESM 模块的 API 更易使用
以前,如果你在 Node 中编写 ESM 模块,则无法从 typescript
包中使用命名导入。
// 报错
import { createSourceFile } from 'typescript'
import * as ts from 'typescript'
ts.createSourceFile // undefined???
ts.default.createSourceFile // 虽行但丑
这是因为 cjs-module-lexer
无法识别 TS 生成的 CJS 代码的模式。此问题已修复,用户现在可以将 TS npm 包中的命名导入与 Node 中的 ESM 模块一起使用。
// 现在有效
import { createSourceFile } from 'typescript'
import * as ts from 'typescript'
ts.createSourceFile // 现在有效
02. 咨询声明文件生成的 package.json
依赖关系
以前,TS 经常会发出类似错误消息:
如果不引用“Y”,则无法命名推断类型“X”。这可能是不可移植的。类型注释是必需的。
这通常是由于 TS 声明文件的生成发现自己位于从未在程序中显式导入的文件内容中。如果路径最终是相对的,则生成对此类文件的导入可能存在风险。
尽管如此,对于 package.json
的 dependencies
或 peerDependencies
和 optionalDependencies
中具有显式依赖关系的代码库,在某些解析模式下生成这样的导入应该是安全的。
因此,在 TS 5.5 中,我们对这种情况更加宽容,且许多类似错误应该会消失。
03. 编辑器和监测模式可靠性改进
TS 添加了一些新功能或修复了现有逻辑,使 --watch
模式和 TS 的编辑器集成感觉更可靠。这有望减少 TS 服务/编辑器的重启。
03-1. 正确刷新配置文件中的编辑器错误
TS 可以为 tsconfig.json
文件生成错误;然而,这些错误实际上是在加载项目时生成的,编辑器通常不会直接为 tsconfig.json
文件请求这些错误。
虽然这听起来像是一个技术细节,但这意味着,当修复所有 tsconfig.json
中的错误时,TS 不会报错新的错误空集,且用户将遗留旧的报错,除非它们重载其编辑器。
TS 5.5 现在故意发射一个事件来清除这些。
03-2. 更好处理删除后立即写入
有些工具不会覆盖文件,而是选择删除它们,然后从零开始创建新文件。举个栗子,运行 npm ci
时就是这种情况。
虽然对于这些工具而言这可能很有效,但对于 TS 的编辑器场景而言可能会造成问题,因为删除 watched 可能会处理它及其所有传递依赖。快速连续删除和创建文件可能会导致 TS 拆除整个项目,然后从零开始重建它。
TS 5.5 现在有一种更细致的方案,保留已删除项目的部分内容,直到它接收到全新的创建事件。这应该使得诸如 npm ci
之类的操作在 TS 中更好。
03-3. 在失败解析中追踪符号链接
当 TS 无法解析模块时,它仍然需要监视任何失败的查找路径,以防稍后添加该模块。
以前,这不是针对符号链接目录实现的,当一个项目中发生构建,但在另一个项目中没有出现时,这可能会在类似 monorepo 的场景中导致可靠性问题。
这应该在 TS 5.5 中修复了,这意味着你不需要经常重启编辑器。
03-4. 项目引用利于自动导入
自动导入不再需要对项目引用(project reference)设置中的依赖项目进行至少一次显式导入。
相反,自动导入补全应该只适用于你在 tsconfig.json
的 references
字段中列出的任何内容。
04. 性能和体积优化
04-1. 语言服务和公共 API 中的单态对象
在 TS 5 中,我们确保 Node
和 Symbol
对象具有一组一致的属性和一致的初始化顺序。这样做有助于减少不同操作中的多态性,允许运行时更快获取属性。
通过这一更改,我们见证了编译器令人喵瞪狗呆的速度提升;然而,这些更改大部分是在我们的数据结构的内部分配器上执行的。语言服务以及 TS 的公共 API 对某些对象使用一组不同的分配器。这使得 TS 编译器更加精简,因为仅用于语言服务的数据永远不会在编译器中使用。
在 TS 5.5 中,对语言服务和公共 API 进行了相同的单态化工作。这意味着,你的编辑器体验以及任何使用 TS API 的构建工具都会获得极快的速度。
事实上,在我们的基准测试中,我们发现使用公共 TS API 分配器时构建时间提速了 5-8%,语言服务操作速度提速了 10-20%。虽然这确实意味着内存增加,但我们认为这种权衡物超所值,并期望找到减少内存开销的方法。
04-2. 单态控制流节点
在 TS 5.5 中,控制流图的节点已被单态化,以便它们始终保持一致的形状。通过这样做,检查时间通常会减少约 1%。
04-3. 控制流图的优化
在许多情况下,控制流分析将遍历不提供任何新信息的节点。我们观察到,在某些节点的 antecedents 或“dominators”中没有任何提前终止或影响的情况下,意味着这些节点始终可以被忽略。
因此,TS 现在构建其控制流图,通过链接到为控制流分析提供有趣信息的早期节点来利用这一点。这会产生一个更扁平的控制流图,更有效地遍历。这种优化带来了一定收益,但某些代码库的构建时间最多减少 2%。
04-4. 减少 TS 打包体积
进一步利用 TS 5 中向模块的过渡,我们通过从通用 API 库导入 tsserver.js
和 typingsInstaller.js
来显著减小 TS 的整体打包体积,而不是让它们各自生成独立的包。
这将 TS 在磁盘上的大小从 30.2 MB 减少到 20.4 MB,并将其打包体积从 5.5 MB 减少到 3.7 MB!
04-5. 声明发出中的节点复用
作为启用 isolatedDeclarations
工作的一部分,我们大幅提高了 TS 在生成声明文件时直接拷贝输入源码的频率。
举个栗子,假设你写了:
export const strBool: string | boolean = 'hello'
export const boolStr: boolean | string = 'world'
粉丝请注意,联合类型是等价的,但联合的顺序不同。当发出声明文件时,TS 有两种等价的可能输出。
第一个是对每种类型使用一致的规范表示:
export const strBool: string | boolean
export const boolStr: string | boolean
第二种是完全按照书面形式复用类型注释:
export const strBool: string | boolean
export const boolStr: boolean | string
出于以下若干原因,第二种方法通常更可取:
- 许多等价表示仍然编码某种程度的意图,最好将其保留在声明文件中
- 生成类型的新表示可能代价高昂,因此最好避免
- 用户编写的类型通常比生成的类型表示形式短
在 TS 5.5 中,我们极大提高了 TS 可以正确识别安全的位置且正确地打印回输入文件中写入的类型的数量。
其中许多情况都是无形的性能改进 —— TS 会生成新的语法节点集,并将它们序列化为字符串。相反,TS 现在可以直接操作原始语法节点,这更快且经济。
05. transpileDeclaration
API
TS 的 API 暴露了一个 transpileModule
函数,旨在让编译单个 TS 代码文件变得容易。因为它无法访问整个程序,所以粉丝请注意,如果代码违反 isolatedModules
选项下的任何错误,它可能不会产生正确的输出。
在 TS 5.5 中,我们添加了一个类似的新 API transpileDeclaration
。此 API 类似于 transpileModule
,但它专门设计用于根据某些输入源文本生成单个声明文件。
就像 transpileModule
一样,它无法访问完整的程序,且存在类似的警告:如果输入代码在新的 isolatedDeclarations
选项下没有错误,它只会生成准确的声明文件。
如果需要,此函数可用于在 isolatedDeclarations
模式下并行化所有文件的声明发射。粉丝请注意,虽然你可能会在 transpileDeclaration
中遭遇 transpileModule
的一些性能开销,但我们正在研究进一步优化此问题的方法。
06. 显著的行为变化
06-1. 禁用 TS 5 废弃功能
TS 5 废除了以下选项和行为:
charset
target: ES3
noImplicitUseStrict
- ......
想要继续使用上述废弃选项,使用 TS 5 及其更新版本的开发者必须指定一个值为 "5.0"
的 ignoreDeprecations
新选项。
在 TS 5.5 中,这些选项行不通了。为了辅助顺利升级,你仍可以在 tsconfig
中指定它们,但在 TS 6 中指定这些选项会报错。
06-2. 在其他模块模式中尊重文件扩展名和 package.json
在 Node 12 支持 ESM 模块之前,TS 从来没有一个好办法来知道它在 node_modules
中找到的 .d.ts
文件是否表示作为 CJS 或 ESM 模块编写的 JS 文件。当绝大多数 npm 只使用 CJS 时,这不会造成大量问题 —— 如果有疑问,TS 可以假设一切都像 CJS。不幸的是,如果这个假设是错误的,它可能会允许不安全的导入:
// node_modules/dep/index.d.ts
export declare function doSomething(): void
// index.ts
// 如果 dep 是 CJS,那就问题不大。
// 如果 dep 是 ESM,即使在打包器中也会失败。
import dep from 'dep'
dep.doSomething()
实际上,这种情况并不常见。但自从 Node 开始支持 ESM 模块以来,ESM 在 npm 上的份额不断增长。
幸运的是,Node 还引入了一种机制,可以辅助 TS 确定文件是 ESM 模块还是 CJS:.mjs
和 .cjs
文件扩展名以及 package.json
字段。
TS 4.7 添加了对理解这些指示、以及编写 .mts
和 .cts
文件的支持;然而,TS 只会读取 --module node16
和 --module nodenext
下的这些指示,因此对于任何使用 --module esnext
和 --moduleResolution bundler
的用户而言,上面的不安全导入仍然是一个问题。
为了解决此问题,TS 5.5 在所有 module
模式中读取并存储由文件扩展名和 package.json
"type"
编码的模块格式信息,并使用它来解决诸如上示所有模式之一(amd
、umd
和 system
除外)。
尊重此格式信息的第二个影响是格式特定的 TS 文件扩展名(.mts
和 .cts
),或在你的项目中显式设置的 package.json 的 "type"
为 commonjs
或 es2015
到 esnext
将覆盖你的 --module
选项。
以前,在技术上可以将 CJS 输出生成到 .mjs
文件中,反之亦然:
// main.mts
export default 'oops'
// $ tsc --module commonjs main.mts
// main.mjs
Object.defineProperty(exports, '__esModule', { value: true })
exports.default = 'oops'
现在,.mts
文件或定义 package.json
的 "type": "module"
作用域的 .ts
文件永远不会发出 CJS 输出,且 .cts
的 package.json 作用域的 .ts
文件永远不会发射 ESM 输出。
06-3. 更严格的装饰器解析
由于 TS 最初引入了装饰器的支持,因此该提案的指定语法已经收紧。TS 现在对其允许的形式更加严格。虽然很罕见,但现有的装饰器可能需要加上括号来避免错误。
class DecoratorProvider {
decorate(...args: any[]) {}
}
class D extends DecoratorProvider {
m() {
class C {
@(super.decorate) // 无括号则报错
method1() {}
}
}
}
06-4. undefined
不再是可定义的类型名称
TS 始终不允许与内置类型冲突的类型别名:
// 非法
type null = any;
// 非法
type number = any;
// 非法
type object = any;
// 非法
type any = any;
由于一个 bug,此逻辑不适用于内置类型 undefined
。在 TS 5.5 中,现在可以正确将其识别为错误:
// 现在也是非法的!
type undefined = any
对名为 undefined
的类型别名的裸引用从一开始就从未真正奏效。你可以定义它们,但不能将它们用作非限定类型名称。
export type undefined = string
export const m: undefined = ''
// TS 5.4 之前的报错
// 无法参考局部定义的 undefined
06-5. 简化 reference 指令声明发射
在生成声明文件时,TS 会按需合成一个 reference 指令。
举个栗子,所有 Node 模块都是在环境中声明的,因此不能单独通过模块解析来加载。诸如此类文件:
import path from 'path'
export const myPath = path.parse(__filename)
会发射一个声明文件,例如:
/// <reference types="node" />
import path from 'path'
export declare const myPath: path.ParsedPath
即使 reference 指令从未出现在源码中。
同样,TS 还按需删除输出中部分非必要的 reference 指令。
举个栗子,假设我们有一个 jest
的 reference 指令;但是,想象一下 reference 指令对于生成声明文件不是必需的。JS 会直接删除它。
所以在下述例子中:
/// <reference types="jest" />
import path from 'path'
export const myPath = path.parse(__filename)
TS 仍会发射为:
/// <reference types="node" />
import path from 'path'
export declare const myPath: path.ParsedPath
在开发 isolatedDeclarations
的过程中,我们意识到对于任何试图在不进行类型检查或使用多个文件上下文的情况下实现声明发射器的人而言,这种逻辑是站不住脚的。
从用户的角度来看,这种行为也很难理解;除非你确切了解类型检查期间发生的情况,否则 reference 指令是否出现在发射的文件中似乎不一致且难以预测。为了防止在启用 isolatedDeclarations
时声明发射有所不同,我们知道我们的发射需要更改。
通过实验,我们发现几乎所有 TS 合成 reference 指令的情况都只是引入 node
或 react
。在这些情况下,期望下游用户已经通过 tsconfig.json 的 "types"
或库导入引用了这些类型,因此不再综合这些 reference 指令不太可能会破坏任何人。
粉丝请注意,这已经是 lib.d.ts
的工作原理了;当模块导出 WeakMap
时,TS 不会合成对 lib="es2015"
的引用,而是假设下游用户已将其包含在其环境中。
对于库作者编写的 reference 指令,进一步的实验表明几乎所有指令都被删除,从未出现在输出中。大多数保留的 reference 指令已被破坏,且可能不打算保留。
鉴于这些结果,我们决定大大简化 TS 5.5 中声明中的引用指令。更加一致的策略将辅助库作者和消费者更好地控制其声明文件。
不再综合 reference 指令。用户编写的 reference 指令将不再保留,除非使用新的 preserve="true"
属性进行注释。
具体而言,输入文件如下:
/// <reference types="some-lib" preserve="true" />
/// <reference types="jest" />
import path from 'path'
export const myPath = path.parse(__filename)
这会发射为:
/// <reference types="some-lib" preserve="true" />
import path from 'path'
export declare const myPath: path.ParsedPath
添加 preserve="true"
向后兼容旧版本的 TS,因为未知属性会被忽略。
这一变化还提高了性能;在我们的基准测试中,启用声明发射的项目的发射阶段提高了 1-4%。
参考文献
- TypeScript:www.typescriptlang.org
- Blog:devblogs.microsoft.com/typescript/…
- GitHub:github.com/microsoft/T…
粉丝互动
本期话题是:如何评价 TS 5.5 公测版?你可以在本文下方自由言论,文明科普。
欢迎持续关注“前端俱乐部”,给前端以福利,给编程以复利。
坚持阅读的小伙伴可以给自己点赞!谢谢大家的点赞,掰掰~
转载自:https://juejin.cn/post/7366646873464176681