可以通过以下 npm 命令开始使用 TypeScript 5.0:
以下是 TypeScript 5.0 的主要更新:
- 全新装饰器
- const 类型参数
- extends 支持多配置文件
- 所有枚举都是联合枚举
- --moduleResolutionbundler
- 自定义解析标志
- --verbatimModuleSyntax
- 支持 export type *
- JSDoc 支持 @satisfies
- JSDoc 支持 @overload
- 编辑器中不区分大小写的导入排序
- 完善 switch/case
- 优化速度、内存和包大小
- 其他重大更改和弃用
全新装饰器
装饰器是即将推出的 ECMAScript 特性,它允许我们以可重用的方式自定义类及其成员。
考虑以下代码:
这里的 greet 方法很简单,在实际中它内部可能会跟复杂,比如需要执行异步逻辑,或者进行递归,亦或是有副作用等。那就可能需要使用 console.log 来调试 greet:
如果有一种方法可以为每种方法做到这一点,可能会很好。
这就是装饰器的用武之地。我们可以编写一个名为 loggedMethod 的函数,如下所示:
这里用了很多 any,可以暂时忽略,这样可以让例子尽可能得简单。
这里,loggedMethod 需要传入一个参数(originalMethod) 并返回一个函数。执行过程如下:
- 打印:LOG: Entering method.
- 将 this 及其所有参数传递给原始方法
- 打印:LOG: Exiting method.
- 返回原始方法的执行结果
现在我们就可以使用 loggedMethod 来修饰 greet 方法:
输出如下:
这里我们在 greet 上面使用了 loggedMethod 作为装饰器——注意这里的写法:@loggedMethod。这样,它会被原始方法和 context 对象调用。因为 loggedMethod 返回了一个新函数,该函数替换了 greet 的原始定义。
loggedMethod 的第二个参数被称为“ context 对象”,它包含一些关于如何声明装饰方法的有用信息——比如它是 #private 成员还是静态成员,或者方法的名称是什么。下面来重写 loggedMethod 以利用它并打印出被修饰的方法的名称。
TypeScript 提供了一个名为 ClassMethodDecoratorContext 的类型,它对方法装饰器采用的 context 对象进行建模。除了元数据之外,方法的 context 对象还有一个有用的函数:addInitializer。这是一种挂接到构造函数开头的方法(如果使用静态方法,则挂接到类本身的初始化)。
举个例子,在JavaScript中,经常会写如下的模式:
或者,greet可以声明为初始化为箭头函数的属性。
编写这段代码是为了确保在greet作为独立函数调用或作为回调函数传递时不会重新绑定。
可以编写一个装饰器,使用addInitializer在构造函数中为我们调用 bind。
bound不会返回任何内容,所以当它装饰一个方法时,它会保留原来的方法。相反,它会在其他字段初始化之前添加逻辑。
注意,我们使用了两个装饰器:@bound和@loggedMethod。这些装饰是以“相反的顺序”运行的。也就是说,@loggedMethod修饰了原始方法greet, @bound修饰了@loggedMethod的结果。在这个例子中,这没有关系——但如果装饰器有副作用或期望某种顺序,则可能有关系。
可以将这些装饰器放在同一行:
我们甚至可以创建返回装饰器函数的函数。这使得我们可以对最终的装饰器进行一些自定义。如果我们愿意,我们可以让loggedMethod
返回一个装饰器,并自定义它记录消息的方式。
如果这样做,必须在使用loggedMethod作为装饰器之前调用它。然后,可以传入任何字符串作为记录到控制台的消息的前缀。
输出结果如下:
装饰器可不仅仅用于方法,还可以用于属性/字段、getter、setter和自动访问器。甚至类本身也可以装饰成子类化和注册。
上面的loggedMethod和bound装饰器示例写的很简单,并省略了大量关于类型的细节。实际上,编写装饰器可能相当复杂。例如,上面的loggedMethod类型良好的版本可能看起来像这样:
我们必须使用this、Args和return类型参数分别建模this、参数和原始方法的返回类型。
具体定义装饰器函数的复杂程度取决于想要保证什么。需要记住,装饰器的使用次数将超过它们的编写次数,所以类型良好的版本通常是更好的——但显然与可读性有一个权衡,所以请尽量保持简单。
const 类型参数
当推断一个对象的类型时,TypeScript通常会选择一个通用类型。例如,在本例中,names 的推断类型是string[]:
通常这样做的目的是实现突变。然而,根据getnames确切的作用以及它的使用方式,通常情况下需要更具体的类型。到目前为止,通常不得不在某些地方添加const,以实现所需的推断:
这写起来会很麻烦,也很容易忘记。在 TypeScript 5.0 中,可以在类型参数声明中添加const
修饰符,从而使类const
推断成为默认值:
注意,const修饰符并不排斥可变值,也不需要不可变约束。使用可变类型约束可能会得到意外的结果。例如:
这里,T的推断候选值是readonly ["a", "b", "c"],而readonly数组不能用于需要可变数组的地方。在这种情况下,推理回退到约束,数组被视为string[],调用仍然成功进行。
更好的定义应该使用readonly string[]:
同样,要记住,const修饰符只影响在调用中编写的对象、数组和基本类型表达式的推断,所以不会(或不能)用const修饰的参数将看不到任何行为的变化:
extends 支持多配置文件
当管理多个项目时,通常每个项目的 tsconfig.json 文件都会继承于基础配置。这就是为什么TypeScript支持extends字段,用于从compilerOptions中复制字段。
但是,在某些情况下,可能希望从多个配置文件进行扩展。例如,想象一下使用一个TypeScript 基本配置文件到 npm。如果想让所有的项目也使用npm中@tsconfig/strictest包中的选项,那么有一个简单的解决方案:将tsconfig.base.json扩展到@tsconfig/strictest:
这在一定程度上是有效的。如果有任何项目不想使用 @tsconfig/strictest,就必须手动禁用这些选项,或者创建一个不从 @tsconfig/strictest 扩展的单独版本的 tsconfig.base.json。
为了提供更多的灵活性,Typescript 5.0 允许extends字段接收多个项。例如,在这个配置文件中:
这样写有点像直接扩展 c,其中 c 扩展 b,b 扩展 a。如果任何字段“冲突”,则后一个项生效。
所以在下面的例子中,strictNullChecks 和 noImplicitAny 都会在最终的 tsconfig.json 中启用。
可以用下面的方式重写最上面的例子:
所有枚举都是联合枚举
当 TypeScript 最初引入枚举时,它只不过是一组具有相同类型的数值常量:
E.Foo 和 E.Bar 唯一的特别之处在于它们可以分配给任何期望类型 E 的东西。除此之外,它们只是数字。
直到 TypeScript 2.0 引入了枚举字面量类型,它赋予每个枚举成员自己的类型,并将枚举本身转换为每个成员类型的联合。它还允许我们只引用枚举类型的一个子集,并缩小这些类型。
给每个枚举成员指定自己的类型有一个问题,即这些类型在某种程度上与成员的实际值相关联。在某些情况下,这个值是不可能计算出来的——例如,枚举成员可以通过函数调用进行初始化。
每当TypeScript遇到这些问题时,它都会悄无声息地退出并使用旧的枚举策略。这意味着要放弃并集和字面量类型的所有优点。
TypeScript 5.0 通过为每个计算成员创建唯一的类型,设法将所有枚举转换为联合枚举。这意味着现在可以缩小所有枚举的范围,并将其成员作为类型引用。
--moduleResolution
TypeScript 4.7 为 --module 和 --moduleResolution 设置引入了 node16 和 nodenext 选项。这些选项的目的是更好地模拟 Node.js 中 ECMAScript 模块的精确查找规则;然而,这种模式有许多其他工具没有真正执行的限制。
例如,在 Node.js 的 ECMAScript 模块中,任何相对导入都需要包含文件扩展名。
在Node.js和浏览器中这样做是有原因的——它使文件查找更快,并且更适合原始文件服务器。但对于许多使用打包工具的开发人员来说,node16/nodenext 的设置很麻烦,因为打包工具没有这些限制中的大部分。在某些方面,node解析模式更适合使用打包工具的人。
但在某些方面,原有的 node 解析模式已经过时了。大多数现代打包工具在 Node.js 中使用 ECMAScript 模块和 CommonJS 查找规则的融合。
为了模拟打包工具是如何工作的,TypeScript 5.0 引入了一个新策略:--moduleResolution bundler
如果正在使用现代打包工具,如 Vite、esbuild、swc、Webpack、Parcel 或其他实现混合查找策略的打包工具,那么新的 bundler 选项应该非常适合你。
另一方面,如果正在编写一个打算在 npm 上发布的库,使用bundler选项可以隐藏不使用bundler的用户可能出现的兼容性问题。因此,在这些情况下,使用node16或nodenext解析选项可能是更好的方法。
自定义解析标志
JavaScript 工具现在可以模拟“混合”解析规则,就像上面描述的打包工具模式一样。由于工具的支持可能略有不同,TypeScript 5.0 提供了启用或禁用一些功能的方法。
allowImportingTsExtensions
--allowImportingTsExtensions 允许 TypeScript 文件使用特定于 TypeScript 的扩展名(如 .ts、.mts 或 .tsx)相互导入。
仅当启用 --noEmit 或 --emitDeclarationOnly 时才允许使用此标志,因为这些导入路径在运行时无法在 JavaScript 输出文件中解析。这里的期望是解析器(例如打包工具、运行时或其他工具)将使 .ts 文件之间的这些导入正常工作。
resolvePackageJsonExports
--resolvePackageJsonExports 强制 TypeScript 在从 node_modules 中的包中读取时查询 package.json 文件的 exports 字段。
resolvePackageJsonImports
--resolvePackageJsonImports 强制 TypeScript 在从其祖先目录包含 package.json 的文件执行以 # 开头的查找时查询 package.json 文件的 imports 字段。
在 --moduleResolution 的 node16、nodenext 和 bundler 选项下,此选项默认为 true。
allowArbitraryExtensions
在 TypeScript 5.0 中,当导入路径以不是已知 JavaScript 或 TypeScript 文件扩展名的扩展名结尾时,编译器将以 {file basename}.d.{extension} 的形式查找该路径的声明文件。例如,如果在打包项目中使用 CSS loader,可能希望为这些样式表编写(或生成)声明文件:
默认情况下,这个导入将引发一个错误,让你知道TypeScript不理解这个文件类型,你的运行时可能不支持导入它。但是,如果已经配置了运行时或打包工具来处理它,则可以使用新--allowArbitraryExtensions编译器选项来抑制错误。
注意,可以通过添加一个名为 app.css.d.ts 而不是 app.d.css.ts 的声明文件通常可以实现类似的效果。然而,这只是通过 Node 对 CommonJS 的 require 解析规则实现的。严格来说,前者被解释为一个名为 app.css.js 的 JavaScript 文件的声明文件。因为相关文件导入需要在 Node 的 ESM 支持中包含扩展名,所以在我们的例子中,TypeScript 会在 --moduleResolution node16 或 nodenext 下的 ESM 文件中出错。
customConditions
--customConditions 获取当 TypeScript 从 package.json 的 [exports] 或 (https://nodejs.org/api/packages.html#exports)) 或 imports 字段解析时应该成功的附加的条件列表。这些条件将添加到解析器默认使用的现有条件中。
例如,当此字段在 tsconfig.json 中设置为:
任何时候在 package.json 中引用 exports 或 imports 字段时,TypeScript 都会考虑名为 my-condition 的条件。
因此,当从具有以下 package.json 的包中导入时:
TypeScript 将尝试查找与foo.mjs对应的文件。这个字段只有在 node16、nodenext 和--modulerresolution为 bundler 时才有效。
--verbatimModuleSyntax
默认情况下,TypeScript 会执行一些称为导入省略的操作。如果这样写:
TypeScript 检测到只对类型使用导入并完全删除导入。输出 JavaScript 可能是这样的:
大多数时候这很好,因为如果 Car 不是从 ./car 导出的值,将得到一个运行时错误。但对于某些边界情况,它确实增加了一层复杂性。例如,没有像 import "./car" 这样的语句,即完全放弃了 import,这实际上对有无副作用的模块产生影响。
TypeScript 的 JavaScript emit 策略也有另外几层复杂性——省略导入并不总是由如何使用 import 驱动的,它通常还会参考值的声明方式。所以并不总是很清楚是否像下面这样的代码:
如果 Car 是用类之类的东西声明的,那么它可以保存在生成的 JavaScript 文件中。但是,如果 Car 仅声明为类型别名或接口,则 JavaScript 文件不应导出 Car。
虽然 TypeScript 可能能够根据来自跨文件的信息做出这些发出决策,但并非每个编译器都可以。
imports 和 exports 的类型修饰符在这些情况下会有帮助。我们可以明确指定import
或export
仅用于类型分析,并且可以在JavaScript文件中使用类型修饰符完全删除。
类型修饰符本身并不是很有用——默认情况下,模块省略仍然会删除导入,并且没有强制区分类型和普通导入和导出。因此 TypeScript 有标志 --importsNotUsedAsValues 以确保使用 type 修饰符,--preserveValueImports 以防止某些模块省略行为,以及 --isolatedModules 以确保 TypeScript 代码适用于不同的编译器。不幸的是,很难理解这 3 个标志的细节,并且仍然存在一些具有意外行为的边界情况。
TypeScript 5.0 引入了一个名为 --verbatimModuleSyntax 的新选项来简化这种情况。规则要简单得多,任何没有 type 修饰符的导入或导出都会被保留。任何使用 type 修饰符的内容都会被完全删除。
有了这个新选项,所见即所得。不过,当涉及到模块互操作时,这确实有一些影响。在此标志下,当设置或文件扩展名暗示不同的模块系统时,ECMAScript 导入和导出不会被重写为 require 调用。相反,会得到一个错误。如果需要生成使用 require 和 module.exports 的代码,则必须使用早于 ES2015 的 TypeScript 模块语法:
虽然这是一个限制,但它确实有助于使一些问题更加明显。例如,忘记在 --module node16 下的 package.json 中设置 type 字段是很常见的。因此,开发人员会在没有意识到的情况下开始编写 CommonJS 模块而不是 ES 模块,从而给出意外的查找规则和 JavaScript 输出。这个新标志确保有意使用正在使用的文件类型,因为语法是有意不同的。
因为 --verbatimModuleSyntax 提供了比 --importsNotUsedAsValues 和 --preserveValueImports 更一致的作用,所以这两个现有标志被弃用了。
支持 export type *
当 TypeScript 3.8 引入仅类型导入时,新语法不允许在 export * from "module" 或 export * as ns from "module" 重新导出时使用。TypeScript 5.0 添加了对这两种形式的支持:
JSDoc 支持 @satisfies
TypeScript 4.9 引入了 satisfies 操作符。它确保表达式的类型是兼容的,而不影响类型本身。以下面的代码为例:
这里,TypeScript 知道 myCompilerOptions.extends 是用数组声明的,因为虽然 satisfies 验证了对象的类型,但它并没有直接将其更改为 CompilerOptions 而丢失信息。所以如果想映射到 extends 上,是可以的。
这对 TypeScript 用户很有帮助,但是很多人使用 TypeScript 来使用 JSDoc 注释对 JavaScript 代码进行类型检查。这就是为什么 TypeScript 5.0 支持一个名为 @satisfies 的新 JSDoc 标签,它做的事情完全一样。
可以捕获类型不匹配:
但它会保留表达式的原始类型,允许稍后在代码中更精确地使用值。
也可以内嵌在任何带括号的表达式上。可以这样写 myCompilerOptions:
这可能在函数调用时更有意义:
JSDoc 支持 @overload
在 TypeScript 中,可以为函数指定重载。重载提供了一种方式,用不同的参数调用一个函数,并返回不同的结果。它可以限制调用者实际使用函数的方式,并优化将返回的结果。
这里,printValue 将字符串或数字作为第一个参数。如果它需要一个数字,它可以使用第二个参数来确定可以打印多少个小数位。
TypeScript 5.0 现在允许 JSDoc 使用新的 @overload 标签声明重载。每个带有 @overload标签的 JSDoc 注释都被视为以下函数声明的不同重载。
现在,无论是在 TypeScript 还是 JavaScript 文件中编写,TypeScript 都可以让我们知道是否错误地调用了函数。
编辑器中不区分大小写的导入排序
在 Visual Studio 和 VS Code 等编辑器中,TypeScript 支持组织和排序导入和导出的体验。但是,对于列表何时“排序”,通常会有不同的解释。
例如,下面的导入列表是否排序?
答案可能是“视情况而定”。如果不关心区分大小写,那么这个列表显然没有排序。字母 f 出现在 t 和 T 之前。
但在大多数编程语言中,排序默认是比较字符串的字节值。JavaScript 比较字符串的方式意味着“Toggle”总是在“freeze”之前,因为根据 ASCII 字符编码,大写字母在小写字母之前。所以从这个角度来看,导入列表是已排序的。
TypeScript 之前认为导入列表是已排序的,因为它会做基本的区分大小写的排序。对于喜欢不区分大小写排序的开发人员,或者使用像 ESLint 这样默认需要不区分大小写排序的工具的开发人员来说,这可能是一个阻碍。
TypeScript 现在默认检测大小写。这意味着 TypeScript 和 ESLint 等工具通常不会就如何最好地对导入进行排序而相互“斗争”。
这些选项最终可能由编辑器配置。目前,它们仍然不稳定且处于试验阶段,现在可以通过在 JSON 选项中使用 typescript.unstable
在 VS Code 中选择加入它们。以下是可以尝试的所有选项(设置为默认值):
完善 switch/case
在编写 switch 语句时,TypeScript 现在会检测被检查的值何时具有字面量类型。以提供更便利的代码快捷输入:
速度、内存和包大小优化
TypeScript 5.0 在代码结构、数据结构和算法实现中包含许多强大的变化。这些都意味着整个体验应该更快——不仅仅是运行 TypeScript,甚至安装它。
以下是相对于 TypeScript 4.9 在速度和大小方面的优势:
图表形式:
TypeScript 包体积变化:
那为什么会有如此大的提升呢?部分优化细节如下:
首先,将 TypeScript 从命名空间迁移到模块,这样就能够利用现代构建工具来执行优化。重新审视了打包策略并删除一些已弃用的代码,已将 TypeScript 4.9 的 63.8 MB 包大小减少了约 26.4 MB。还通过直接函数调用带来了显著的速度提升。
在将信息序列化为字符串时,执行了一些缓存。类型显示可能作为错误报告、声明触发、代码补全等的一部分发生,最终可能会相当昂贵。TypeScript 现在缓存了一些常用的机制以在这些操作中重用。
总的来说,预计大多数代码库应该会看到 TypeScript 5.0 的速度提升,并且始终能够重现 10% 到 20% 之间的提升。当然,这将取决于硬件和代码库特性。
其他重大更改和弃用
运行时要求
TypeScript 现在的 target 是 ECMAScript 2018。TypeScript 软件包还将预期的最低引擎版本设置为 12.20。对于 Node.js 用户来说,这意味着 TypeScript 5.0 需要至少Node.js 12.20 或更高版本才能运行。
lib.d.ts 变化
更改 DOM 类型的生成方式可能会对现有代码产生影响。注意,某些属性已从数字转换为数字字面量类型,并且用于剪切、复制和粘贴事件处理的属性和方法已跨接口移动。
API 重大变更
在 TypeScript 5.0 中, 转向了模块,删除了一些不必要的接口,并进行了一些正确性改进。
关系运算符中的禁止隐式强制
如果编写的代码可能导致隐式字符串到数字的强制转换,TypeScript 中的某些操作现在会进行警告:
在 5.0 中,这也将应用于关系运算符 >、<、<= 和 >=:
如果需要这样做,可以使用+
显式地将操作数转换为数字:
弃用和默认更改
在 TypeScript 5.0 中,弃用了以下设置和设置值:
- --target: ES3
- --out
- --noImplicitUseStrict
- --keyofStringsOnly
- --suppressExcessPropertyErrors
- --suppressImplicitAnyIndexErrors
- --noStrictGenericChecks
- --charset
- --importsNotUsedAsValues
- --preserveValueImports
在 TypeScript 5.5 之前,这些配置将继续被允许使用,届时它们将被完全删除,但是,如果正在使用这些设置,将收到警告。在 TypeScript 5.0 以及未来版本 5.1、5.2、5.3 和 5.4 中,可以指定 "ignoreDeprecations": "5.0" 以消除这些警告。TypeScript 团队很快会发布一个 4.9 补丁,允许指定 ignoreDeprecations 以实现更平滑的升级。除了弃用之外,还更改了一些设置以更好地改进 TypeScript 中的跨平台行为。
- --newLine,控制 JavaScript 文件中发出的行结束符,如果没有指定,过去是根据当前操作系统推断的。我们认为构建应该尽可能确定,Windows 记事本现在支持换行符,所以新的默认设置是 LF。旧的特定于操作系统的推理行为不再可用。
- --forceConsistentCasingInFileNames,它确保项目中对相同文件名的所有引用都在大小写中达成一致,现在默认为 true。这有助于捕获在不区分大小写的文件系统上编写的代码的差异问题。
参考资料
- https://devblogs.microsoft.com/typescript/announcing-typescript-5-0-rc/。
- https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/。