TypeScript 5.0 已发布!看看增加了什么新功能
本文作者是蚂蚁集团前端工程师厚发,基于《TypeScript 5.0 Beta 发布》,结合最新的发布说明内容,重新修改的。TypeScript 正式迎来 5.0 时代。
自 Beta 版本以来,有几个显著的更改
其中一个新变化是 TypeScript 允许在 export 和 export default 之前或之后放置装饰器。这一变化反映了 TC39(ECMAScript/JavaScript 的标准组织)内的讨论和共识。
另一个变化是,新的模块解析选项(moduleResolution)“bundler” 现在只能在将 --module
选项设置为 esnext
时使用。这是为了确保在 bundler 解析 import
语句之前,不管 bundler 或加载器(loader)是否使用 TypeScript 的模块选项,输入文件中编写的 import
语句都不会转换为 require
调用。在这些发行说明中,我们也提供了一些上下文信息,建议大多数库作者使用 node16
或 nodenext
。
尽管 TypeScript 5.0 Beta 版本中已经具备了此功能,但我们没有为支持编辑器场景中不区分大小写的导入排序编写文档。这在一定程度上是因为自定义 UX 仍在讨论中,但是默认情况下,TypeScript 现在应该与您的其他工具更好地配合使用。具体介绍在后面。
自我们发布 RC 版本以来,最显着的变化是 TypeScript 5.0 现在在 package.json 中指定了 Node.js 的最低版本为 12.20。我们还发布了一篇有关 TypeScript 5.0 迁移到模块的文章,并链接到了它。(https://devblogs.microsoft.com/typescript/typescripts-migration-to-modules/)
自 TypeScript 5.0 Beta 和 RC 发布以来,速度基准测试和包大小差异的具体数字也已经进行了调整,尽管噪声是运行的一个因素。一些基准测试的名称也已经进行了调整以提高清晰度,并且包大小的改进已经移动到一个单独的图表中。
BreakChanges And Deprecations
运行时要求
要求 Node.js 10.x 以上。
lib.d.ts变更
例行环节。具体变更在这:lib.d.ts change (https://github.com/microsoft/TypeScript/pull/52328/files)
后面实际项目中如果有遇到一些常用的用法报错了,需要手动更改代码的 case,笔者会在这里补充,目前还没发现。
关系运算符中禁止隐式类型转换
5.0 之前 TypeScript 只会检查 +
-
*
/
运算符的隐式类型转换,提示类型报错。
5.0 之后关系运算符 >
<
<=
>=
也检查。
经典操作,可以通过 +
运算符来进行进行转换。
enum 类型检修
自从 TypeScript 支持 enum 枚举类型以来,一直都有些长期存在的奇怪的问题。官方说在 5.0,他们正在处理解决这些问题。
给 enum 类型变量赋值字面量时,如果超出了 enum 定义范围,会报错
注意是字面量。下面这种情况还是不会报错:
修复之前的一个问题:由多个 enum 类型组成的混合 enum 类型,enum 的成员都会是 number 类型
官方的例子:
修饰器
ECMASCript 的修饰器标准已经进到了 Stage3 阶段了,TypeScript 5.0 落地了 ECMASCript 的修饰器标准。修饰器简单来说是一个函数,可以用于修饰类、类的成员方法/属性。
修饰器使用说明
官方以一个类方法修饰器来举了个例子:
代码中,loggedMethod
就是一个类方法修饰器。它是一个函数,函数的第一个入参是被修饰的greeet
方法的初始值,第二个入参context
是上下文信息。修饰器最后返回的是replacementMethod
函数,当被修饰的方法greet
被调用时,执行的就是replacementMethod
函数。我们可以把一些可复用的代码逻辑放到loggedMethod
修饰器中,那就可以基于修饰器去复用代码了。
修饰器上下文
非常值得关注的是,第二个入参context
提供了一个上下文信息对象。根据修饰器的种类不同有不同的类型:类修饰器:ClassDecoratorContext
、类方法修饰器:ClassMethodDecoratorContext
、类属性getter修饰器:ClassGetterDecoratorContext
、类属性setter修饰器:ClassSetterDecoratorContext
、类属性修饰器:ClassFieldDecoratorContext
、类accessor修饰器:ClassAccessorDecoratorContext
context
的类型定义大致是这样的:
addInitializer(新语法)
上下文中的addInitializer
方法是标准中的新语法。官方文档解释:
It’s a way to hook into the beginning of the constructor (or the initialization of the class itself if we’re working with statics)
当修饰器用来修饰非 static 属性/方法时,可以通过这个方法在实例初始化时,构造函数执行之前指定执行逻辑。当修饰器用来修饰 static 属性/方法时,可以通过这个方法类初始化时,指定执行逻辑。举个例子就清晰很多:
Initializer
方法执行时机
类的静态属性/方法初始化,在类初始化过程中。
staticMethodDecorator
、staticFiledDecorator
中通过addInitializer
方法增加的初始化函数,先执行。
classFiledDecorator
中通过addInitializer
方法增加的初始化函数,在类初始化之后执行。类进行实例化
instanceMethod
、instanceFiledDecorator
中通过addInitializer
方法增加的初始化函数,在实例初始化时,构造函数执行之前执行。
Initializer
方法的应用
官网例子:使用addInitializer()
绑定this
。
与之前版本实验性的修饰器的不同
之前版本的 TypeScript 也支持修饰器(https://typescript.bootcss.com/decorators.html),需要增加--experimentalDecorators
编译选项。
TypeScript 5.0 的修饰器标准跟之前的修饰器是不兼容的。旧版的 --experimentalDecorators
选项将会仍然保留,如果启用此配置,则仍然会将装饰器视为旧版,新版的装饰器无需任何配置就能够默认启用。
TypeScript5.0 的修饰器标准跟之前的元数据反射(https://www.typescriptlang.org/docs/handbook/decorators.html#metadata)是不兼容的。
写类型完备的修饰器
推荐写类型完备的修饰器。如果写类型完备的修饰器,不免会用到很多泛型、类型参数,这样也会影响代码的可读性。怎么写修饰器后面会有更多的文档出来。先推荐了一篇文章:《JavaScript metaprogramming with the 2022-03 decorators API》https://2ality.com/2022/10/javascript-decorators.html
const Type Parameters
提供const
修饰符,对泛型的类型参数进行修饰。用于解决之前 需要 增加as const
断言才能实现的类型推导。
5.0 之前对于泛型的类型参数的类型推导,TypeScript 往往只能推导到基础数据类型,这样做是保证变量类型的可变。例子:
但是,从getConstValue
函数真实的意图--得到一个不可变的常量,来说这样的类型推导是不完备的。之前我们往往这么做:
在5.0版本中,可以不需要这样的骚操作了,把const
修饰符加上作用于泛型的类型参数即可。优雅!
与泛型约束一起使用
注意当const
修饰符修饰的类型变量,后面带有泛型约束extends
时,如泛型约束不是常量,那么类型推导的结果遵循泛型约束,而不是常量。
官网的例子:
作用范围
const
修饰符的类型推导生效范围:函数调用时,参数是对象、数组或表达式。如果函数调用时,参数是一个变量,上面讲述的类型推导不会生效。
官网例子:
compilerOptions 的 extends 配置支持多文件
枚举
TypeScript 5.0 之前的枚举,枚举分为数字枚举和字符串枚举。
TypeScript 5.0 将所有枚举合并为统一的一种枚举类型(Union enums),其含义就是枚举类型是其所有枚举成员类型组成的联合类型。
这样做带来什么改变呢?下面列举了一些 5.0 之前,在使用枚举时,会碰到的一些很神奇诡异的规则,然后跟 5.0 之后做一下对比,看有什么改变。
前后对比
5.0 之前:把枚举成员用作类型,所有枚举成员必须使用字面量初始化。
5.0 之后:没有这个约束了。
github 相关的#27976(https://github.com/microsoft/TypeScript/issues/27976)
UserResponse
枚举中,存在UserResponse.Yes``UserResponse.NotSure
两个用非字面量初始化的成员,那么使用成员去当TypeScript中的类型使用时,就会报错:
Enum type 'UserResponse' has members with initializers that are not literals.(2535)
必须把枚举成员全部使用字面量来初始化,可以对比着UserResponse_1
来看。
5.0 之前:字符串枚举成员只能是常量枚举成员。比如,无法使用字符串变量或者模版字符串给枚举成员赋值。
5.0 之后:字符串枚举成员可以是计算枚举成员。
5.0 之前:枚举成员的初始化,存在一些约束。
5.0 之后:约束的第二条由「数字字面量」放宽到「数字字面量和数字常量」。其他的计算枚举成员还是会报错。
(感觉规则又变复杂了。不过用起来会方便一点。)
疑问
疑问一
那问题来了,5.0 版本之后,类似E_1.a
、E_1.c
这样(使用数字常量初始化或带有数字常量表达式)的枚举成员,是「计算枚举成员」呢还是「常量枚举成员」。
按照「计算枚举成员」和「常量枚举成员」的定义(https://www.typescriptlang.org/docs/handbook/enums.html#computed-and-constant-members)
,它是「计算枚举成员」按照枚举成员的初始化约束,它又是「常量枚举成员」
感觉「计算枚举成员」和「常量枚举成员」的定义(https://www.typescriptlang.org/docs/handbook/enums.html#computed-and-constant-members)要修改,到目前官方文档还没修改。
moduleResolution 配置新增 bundler支持
TypeScript 4.7 (https://devblogs.microsoft.com/typescript/announcing-typescript-4-7/#esm-nodejs)为 --module
和 --moduleResolution
增加了node16
和nodenext
,从而更好地在 Nodejs 中支持 ESM 标准。但是在这种模式下,会有很多限制。
比如,在 Nodejs 中,ESM(ECMAScript module)要求在import
相对路径的依赖时,需要显示写文件的扩展名。
这样做的原因是为了在文件服务器中有更好的文件搜寻速度。对比使用其他构建工具,node16/nodenext
模式下的这些限制还是太麻烦了,甚至说默认的node
模式更好。
但是,默认的node
模式已经过时了,大多数现代构建工具混合着使用ESM(ECMAScript module)和CommonJS两种模块标准的模块解析策略。
于是,5.0 中 TypeScript 提供了新的moduleResolution
配置选项bundler
,它同时兼容 ESM(ECMAScript module) 和 CommonJS 两种模块标准的模块解析策略,但是又没有 ESM 在 Nodejs 中的限制。
跟 moduleResolution 相关的几个配置项
allowImportingTsExtensions
配置启用后,import
其他模块时允许携带.ts
, .mts
和 .tsx
这三种扩展名。这个配置启用必须同时与--noEmit
--emitDeclarationOnly
这两个配置一起启用。
当--moduleResolution
是node16
、nodenext
、bundler
是,配置默认启用。
resolvePackageJsonExports与resolvePackageJsonImports
配置启用后,import
来自node_modules
中的模块时,TypeScript 会去解析模块对应的package.json
中的exports
和imports
字段。这块可以去看一下 Conditional exports (https://nodejs.org/api/packages.html#conditional-exports)的知识。当--moduleResolution
是node16
、nodenext
、bundler
是,配置默认启用。
allowArbitraryExtensions
允许任意的后缀名。当在代码里 import 的模块扩展名不是.js``.jsx``.ts``.tsx
,编译器会按以下的规则去查找该模块的类型定义文件:{file basename}.d.{extension}.ts
。
官网例子:
默认的话,TypeScript 会提示一个错误,让你知道 TypeScript 无法解析这种文件类型,代码运行时可能无法正确地导入。但是如果你在 bundler 中正确地配置,可以加上--allowArbitraryExtensions
这个新的编译选项来阻止错误的提示。
在前端编写 CSS Modules 的时候,以前是这样处理 import css/less 的。
现在通过这个编译选项可以增加上类型支持了。但是,针对 css/less 等样式文件,我可能还是会这样处理,感觉加上类型没太大必要。
当--moduleResolution
是node16
、nodenext
、bundler
是,配置默认启用。
customConditions
Conditional exports中还支持自定义conditions (https://nodejs.org/api/packages.html#resolving-user-conditions)。通过这个配置进行支持。
当我们需要import
一个模块es-module-package
,它的package.json
是下面这样定义的话
那么tsconfig.json
可以这么写
当--moduleResolution
是node16
、nodenext
、bundler
是,配置默认启用。
verbatimModuleSyntax
提供了一个新的配置项,用于简化之前 TypeScript 的引用省略功能涉及importsNotUsedAsValues
、preserveValueImports
、isolatedModules
三个配置项的配置复杂问题。
支持 export type *
支持使用export * from "module"
和export * as ns from "module"
这样的语句进行类型导出(https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export)。
JSDoc 支持 @satisfies
TypeScript 4.9 的时候支持了[satisfies](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html)
操作符。现在在 JSDoc 上也支持@satisfies
注释,因为有部分开发者是通过 JSDoc 注释来给 Javascript 提供类型检查的。
JSDoc 支持 @overload
TypeScript 中你可以定义同一个函数的不同的入参类型或者不同的返回值类型。现在在 JSDoc 上支持通过@overload
注释来满足这个需求。
tsc build 模式下支持设置以下几个标志位
--declaration
--emitDeclarationOnly
--declarationMap
--soureMap
--inlineSourceMap
编辑器中的不区分大小写的导入排序
在类似于 Visual Studio 和 VS Code 这样的编辑器中,TypeScript 为组织和排序导入和导出提供支持。不过,对于何时排序的列表会有不同的解释。
例如,以下导入列表是否已排序?
令人惊讶的是,答案可能是“取决于情况”。如果我们不关心大小写,那么这个列表显然没有排序。字母 "f" 在 "t" 和 "T" 之前。
但在大多数编程语言中,排序默认比较字符串的字节值。JavaScript 比较字符串的方式意味着 "Toggle" 总是排在 "freeze" 之前,因为根据 ASCII 字符编码 (https://en.wikipedia.org/wiki/ASCII),大写字母在小写字母之前。因此,从这个角度来看,导入列表已排序。
TypeScript 以前认为导入列表已排序,因为它进行了基本的区分大小写排序。这可能是开发人员的痛点,因为他们更喜欢不区分大小写的排序方式,或者使用像 ESLint 这样的工具默认需要不区分大小写的排序方式。
TypeScript 现在默认检测大小写敏感性。这意味着 TypeScript 和类似 ESLint 的工具通常不会因为如何最好地排序导入而“争执”。
我们的团队还在尝试更多的排序策略,您可以在此处阅读更多信息(https://github.com/microsoft/TypeScript/pull/52115)。这些选项可能最终可以由编辑器进行配置。目前,它们仍然是不稳定和实验性的,您可以通过在 VS Code 中使用 JSON 选项中的 typeScript.unstable
条目来选择它们。以下是您可以尝试的所有选项(默认设置):
完善的 switch/case 代码补全

速度、内存和包体积的优化
官方称 5.0 版本,不管是代码结构、数据结构还是算法实现都进行了很“强大”的改变。在使用 TypeScript 的时候,感觉会变得更快,不仅仅是在运行 TypeScript 的时候,甚至在安装它的时候都会觉得更快。
举了两个例子:
使用 TypeScript 5.0 Beta 构建 VS Code 花的时间只占了使用 4.9 去构建的 81%。
TypeScript 5.0 Beta(37.3MB) 包体积是 4.9(63.8MB)的 58%。
TypeScript 5.0 更新了一些值得注意的改进,下面我们会逐一介绍。
首先,我们最近将 TypeScript 从命名空间转移到了模块中,这使我们能够利用现代构建工具来执行优化,如作用域提升。使用这些工具,重新审视我们的打包策略,并删除一些废弃的代码,使 TypeScript 4.9 的63.8 MB 包大小减小了约 26.4 MB。这也通过直接函数调用带来了显着的加速。我们在这里写了一篇关于我们迁移到模块的详细说明(https://devblogs.microsoft.com/typescript/typescripts-migration-to-modules/)。
TypeScript 还增加了对编译器内部对象类型的更一致性,并在一些对象类型上减少了存储的数据。这减少了多态操作,同时平衡了由于使我们的对象结构更统一而带来的内存使用增加。
我们还对序列化信息到字符串时进行了一些缓存。类型显示,可能会作为错误报告、声明发出、代码完成等的一部分发生,可能会相当昂贵。TypeScript现在对一些常用的机制进行了缓存,以便在这些操作之间重复使用。
我们所做的另一个值得注意的改变是,利用var
关键字偶尔规避在闭包中使用let
和const
所产生的成本,这提高了我们的一些解析性能。
总的来说,我们期望大多数代码库应该会从 TypeScript 5.0 中看到速度的提升,并且一直能够重复获得 10% 到 20% 的胜利。当然,这将取决于硬件和代码库的特征,但我们鼓励您今天就在您的代码库上尝试它!
有关更多信息,请参见我们的一些值得注意的优化:
迁移到模块 https://github.com/microsoft/TypeScript/pull/51387
节点单态化 https://github.com/microsoft/TypeScript/pull/51682
Symbol 单态化 https://github.com/microsoft/TypeScript/pull/51880
标识符大小减小 https://github.com/microsoft/TypeScript/pull/52170
Printer 缓存 https://github.com/microsoft/TypeScript/pull/52382
有限使用 var https://github.com/microsoft/TypeScript/issues/52924
参考
《Announcing TypeScript 5.0》
https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/
《Announcing TypeScript 5.0 Beta》
https://devblogs.microsoft.com/typescript/announcing-typescript-5-0-beta/