[译] TypeScript 5.0 发布
原文地址:Announcing TypeScript 5.0
2023.3.16 by Daniel Rosenwasser and the TypeScript Team
今天,我们很高兴地宣布 TypeScript 5.0 的发布!
此版本带来了许多新功能,同时旨在使 TypeScript 更小、更简单、更快。我们已经实施了新的装饰器标准,添加了更好地支持 Node 和 bundlers 中的 ESM 项目的功能,为库作者提供了控制泛型推理的新方法,扩展了我们的 JSDoc 功能,简化了配置,并进行了许多其他改进。
如果您还不熟悉 TypeScript,它是一种基于 JavaScript 的语言,通过添加类型语法来进行类型检查。类型检查可以帮助发现许多常见错误,从错别字到逻辑错误。将类型引入 JavaScript 还使我们能够构建出色的工具,因为类型可以在您喜欢的编辑器中支持代码补全、直接定义和重构等功能。事实上,如果您使用过 Visual Studio 或 VS Code 等编辑器,TypeScript 已经提供了 JavaScript 体验!您可以在 typescriptlang.org 上了解这门语言。
但是,如果您已经熟悉 TypeScript,也不用担心!5.0 不是破坏性版本,你所知道的一切仍然适用。虽然 TypeScript 5.0 包括正确性更改和一些不常用选项的弃用,但我们相信大多数开发人员都会有与以前版本类似的升级体验。
要开始使用 TypeScript 5.0,您可以通过 NuGet获取它,或者使用 npm 和下面的命令:
npm install -D typescript
你也可以按照指示在 Visual Studio Code 中使用新版本的 TypeScript 的说明进行操作。
这是 TypeScript 5.0 中新功能的快速列表!
- 装饰器
const
泛型参数extends
支持多个配置文件- 所有的枚举都是联合枚举
--moduleResolution 配置新增 bundler 支持
- 自定义解析标志
--verbatimModuleSyntax
- 支持
export type *
- JSDoc 支持
@satisfies
- JSDoc 支持
@overload
- 运行 tsc
--build
可以传入的新指令 - 编辑器中不区分大小写的导入排序
switch
/case
语法补足- 速度、内存和包大小优化
- 重大更改和弃用
- 下一步是什么?
自 Beta 和 RC 以来有什么新功能?
自 beta 版发布以来, TypeScript 5.0 有几个显著的变化。
自 TypeScript 5.0 Beta 以来,一个新区别是 TypeScript 允许将装饰器放置在 export
和 export default
之前或之后。这一变化反映了 TC39(ECMAScript/JavaScript 的标准机构)内部的讨论和共识。
另一个是新的 bundler
模块解析选项只能在 --module
选项设置为 esnext
时使用。这样做是为了确保在输入文件中写入的 import
语句不会在捆绑器解析它们之前转换为 require
调用,无论捆绑器或加载器是否遵从 TypeScript 的 module
选项。我们还在这些发布说明中提供了一些上下文,建议大多数库作者坚持使用 node16
or nodenext
。
虽然 TypeScript 5.0 Beta 附带了此功能,但我们没有记录我们在编辑器场景中支持不区分大小写的导入排序的工作。这部分是因为用于自定义的 UX 仍在讨论中,但默认情况下,TypeScript 现在应该可以更好地与您的其他工具一起使用。
自我们的 RC 以来,我们最显著的变化是 TypeScript 5.0 现在在 package.json
中指定了 Node.js 的最低版本为 12.20
。 我们还发布了一篇关于 TypeScript 5.0 向 modules 迁移的文章,并提供了链接。
自 TypeScript 5.0 Beta 和 RC 发布以来,速度基准和包大小增量的具体数字也进行了调整,尽管噪音一直是运行过程中的一个因素。为了清晰起见,还对一些基准的名称进行了调整,包大小的改进也被移至单独的图表中。
装饰器
装饰器是即将推出的 ECMAScript 功能,它允许我们以可重用的方式自定义类及其成员。
让我们思考以下代码:
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
const p = new Person("Ron");
p.greet();
greet
这里很简单,但让我们想象它更复杂——也许它执行一些异步逻辑,它是递归的,它有副作用等等。不管你想象的是哪种场景,假设你抛出了一些 console.log
调用来帮助调试 greet
。
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
greet() {
console.log("LOG: Entering method.");
console.log(`Hello, my name is ${this.name}.`);
console.log("LOG: Exiting method.")
}
}
这种模式相当普遍。如果有一种方法我们可以为每种方法做到这一点,那就太好了!
这就是装饰器的用武之地。我们可以编写一个 loggedMethod
的函数,如下所示:
function loggedMethod(originalMethod: any, _context: any) {
function replacementMethod(this: any, ...args: any[]) {
console.log("LOG: Entering method.")
const result = originalMethod.call(this, ...args);
console.log("LOG: Exiting method.")
return result;
}
return replacementMethod;
}
“这些 any
都是怎么回事?这是什么,any
Script!?”
请耐心等待——我们暂时保持简单,以便我们可以专注于此函数在做什么。请注意,loggedMethod
接收原始方法 ( originalMethod
) 并返回一个函数
- 输出“Entering...”日志
this
将其所有参数传递给原始方法- 输出一条“Exiting...”日志,并且
- 返回原始方法返回的任何内容。
现在我们可以使用 loggedMethod
来装饰方法 greet
:
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
@loggedMethod
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
const p = new Person("Ron");
p.greet();
// Output:
//
// LOG: Entering method.
// Hello, my name is Ron.
// LOG: Exiting method.
我们只是在 greet
上面使用了 loggedMethod
作为装饰器 ——注意我们把它写成了 @loggedMethod
。当我们这样做时,它会被 target 方法和 context 对象调用。因为 loggedMethod
返回了一个新函数,该函数替换了greet
.
我们还没有提到它,而是 loggedMethod
用第二个参数定义的。它被称为“上下文对象”,它有一些关于如何声明修饰方法的有用信息——比如它是 #private
成员还是静态成员,或者方法的名称是什么。让我们重写 loggedMethod
以利用它并打印出被装饰的方法的名称。
function loggedMethod(originalMethod: any, context: ClassMethodDecoratorContext) {
const methodName = String(context.name);
function replacementMethod(this: any, ...args: any[]) {
console.log(`LOG: Entering method '${methodName}'.`)
const result = originalMethod.call(this, ...args);
console.log(`LOG: Exiting method '${methodName}'.`)
return result;
}
return replacementMethod;
}
我们现在使用 context 参数——它是 loggedMethod
中第一个具有比 any
和 any[]
更严格的参数类型。TypeScript 提供了一个名为 ClassMethodDecoratorContext
的类型,他对方法装饰器所接收的上下文对象进行建模。
除了元数据之外,方法的上下文对象还有一个有用的函数,称为 addInitializer
。 这是一种挂钩到构造函数开头的方法(如果我们使用static
,则挂钩到类本身的初始化)。
例如——在 JavaScript 中,通常会编写类似以下模式的内容:
class Person {
name: string;
constructor(name: string) {
this.name = name;
this.greet = this.greet.bind(this);
}
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
或者,greet
可以声明为初始化为箭头函数的属性。
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
greet = () => {
console.log(`Hello, my name is ${this.name}.`);
};
}
编写此代码是为了确保在 greet
作为独立函数调用或作为回调传递 this
时不会重新绑定。
const greet = new Person("Ron").greet;
// We don't want this to fail!
greet();
我们可以编写一个装饰器,使用 addInitializer
在构造函数中调用 bind
。
function bound(originalMethod: any, context: ClassMethodDecoratorContext) {
const methodName = context.name;
if (context.private) {
throw new Error(`'bound' cannot decorate private properties like ${methodName as string}.`);
}
context.addInitializer(function () {
this[methodName] = this[methodName].bind(this);
});
}
bound
不返回任何东西——所以当它装饰一个方法时,它会保留原来的方法。相反,它将在任何其他字段初始化之前添加逻辑。
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
@bound
@loggedMethod
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
const p = new Person("Ron");
const greet = p.greet;
// Works!
greet();
请注意,我们堆叠了两个装饰器—— @bound
和 @loggedMethod
。这些装饰以“相反的顺序”运行。即 @loggedMethod
装饰原始方法 greet
,@bound
装饰 @loggedMethod
的结果 。在此示例中,这并不重要——但如果您的装饰器有副作用或期望特定顺序,则可能会发生这种情况。
同样值得注意的是:根据你喜欢代码风格,可以将这些装饰器放在同一行。
@bound @loggedMethod greet() {
console.log(`Hello, my name is ${this.name}.`);
}
可能不太明显的是,我们甚至可以创建返回装饰器函数的函数。这使得定制最终的装饰器成为可能。如果我们愿意,我们可以让 loggedMethod
返回一个装饰器并自定义它记录消息的方式。
function loggedMethod(headMessage = "LOG:") {
return function actualDecorator(originalMethod: any, context: ClassMethodDecoratorContext) {
const methodName = String(context.name);
function replacementMethod(this: any, ...args: any[]) {
console.log(`${headMessage} Entering method '${methodName}'.`)
const result = originalMethod.call(this, ...args);
console.log(`${headMessage} Exiting method '${methodName}'.`)
return result;
}
return replacementMethod;
}
}
如果我们这样做,我们必须在使用 loggedMethod
作为装饰器之前调用它。然后我们可以传入任何字符串作为输出到控制台的日志的前缀。
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
@loggedMethod("")
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
const p = new Person("Ron");
p.greet();
// Output:
//
// Entering method 'greet'.
// Hello, my name is Ron.
// Exiting method 'greet'.
装饰器不仅仅可以用在方法上!它们可用于属性/字段、getter、setter 和自动访问器。甚至类本身也可以为子类化和注册之类的事情进行装饰。
要深入了解有关装饰器的更多信息,您可以阅读 Axel Rauschmayer 的详尽摘要。
有关涉及的更改的更多信息,您可以查看原始 pull request。
与实验性遗留装饰器的差异
如果您已经使用 TypeScript 一段时间,您可能会意识到它多年来一直支持“实验性”装饰器。虽然这些实验性装饰器非常有用,但它们模拟了一个更旧版本的装饰器提案,并且始终需要一个名为 --experimentalDecorators
。 任何在没有此标志的情况下尝试在 TypeScript 中使用装饰器都会提示错误消息。
--experimentalDecorators
在可预见的未来将继续存在;然而,如果没有这个标志,装饰器现在将成为所有新代码的有效语法。在之外--experimentalDecorators
,它们将以不同方式进行类型检查和释放。类型检查规则和 emit 完全不同,虽然可以编写装饰器来支持旧的和新的装饰器行为,但任何现有的装饰器函数都不太可能这样做。
这个新的装饰器提案与 --emitDecoratorMetadata
不兼容,它不允许装饰参数。未来的 ECMAScript 提案可能会帮助弥合这一差距。
最后一点:除了允许将装饰器放在 export
关键字之前,装饰器提案现在还提供了在 export
或 export default
之后放置装饰器的选项。唯一的例外是不允许混合使用这两种样式。
// allowed
@register export default class Foo {
// ...
}
// also allowed
export default @register class Bar {
// ...
}
// error - before *and* after is not allowed
@before export @after class Bar {
// ...
}
编写类型良好的装饰器
上面的 loggedMethod
和 bound
装饰器示例有意简单化并省略了很多关于类型的细节。
键入装饰器可能相当复杂。例如,上面的类型正确的版本 loggedMethod
可能看起来像这样:
function loggedMethod<This, Args extends any[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
const methodName = String(context.name);
function replacementMethod(this: This, ...args: Args): Return {
console.log(`LOG: Entering method '${methodName}'.`)
const result = target.call(this, ...args);
console.log(`LOG: Exiting method '${methodName}'.`)
return result;
}
return replacementMethod;
}
我们必须使用类型参数 This
、Args
和 Return
分别定义 this
的类型、参数和原始方法的返回类型。
装饰器函数定义的具体复杂程度取决于您要保证的内容。请记住,您的装饰器将被使用的次数多于它们被编写的次数,因此类型良好的版本通常更可取——但显然需要与可读性进行权衡,因此请尽量保持简单。
将来会提供更多关于编写装饰器的文档——但这篇文章应该有大量关于装饰器机制的细节。
const
泛型参数
在推断对象的类型时,TypeScript 通常会选择一种通用的类型。例如,在本例中,names
的推断类型是 string[]
:
type HasNames = { readonly names: string[] };
function getNamesExactly<T extends HasNames>(arg: T): T["names"] {
return arg.names;
}
// Inferred type: string[]
const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"]});
通常这样做的目的是使突变成为可能。
但是,根据 getNamesExactly
的具体功能和用途,通常需要更具体的类型。
到目前为止,API 作者通常不得不建在某些地方添加 as const
以实现所需的推理:
// The type we wanted:
// readonly ["Alice", "Bob", "Eve"]
// The type we got:
// string[]
const names1 = getNamesExactly({ names: ["Alice", "Bob", "Eve"]});
// Correctly gets what we wanted:
// readonly ["Alice", "Bob", "Eve"]
const names2 = getNamesExactly({ names: ["Alice", "Bob", "Eve"]} as const);
这可能很麻烦且容易忘记。在 TypeScript 5.0 中,您现在可以将 const
修饰符添加到类型参数声明中,以使const
-like 推理成为默认值:
type HasNames = { names: readonly string[] };
function getNamesExactly<const T extends HasNames>(arg: T): T["names"] {
// ^^^^^
return arg.names;
}
// Inferred type: readonly ["Alice", "Bob", "Eve"]
// Note: Didn't need to write 'as const' here
const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"] });
请注意,const
修饰符不拒绝可变值,也不需要不可变约束。使用可变类型约束可能会产生令人惊讶的结果。例如:
declare function fnBad<const T extends string[]>(args: T): void;
// 'T' is still 'string[]' since 'readonly ["a", "b", "c"]' is not assignable to 'string[]'
fnBad(["a", "b" ,"c"]);
在这里,推断的候选项 T
是 readonly ["a", "b", "c"]
,并且 readonly
不能在需要可变数组的地方使用数组。在这种情况下,推理回退到约束条件,数组被视为 string[]
,并且调用仍然成功进行。
此函数的更好定义应该使用 readonly string[]
:
declare function fnGood<const T extends readonly string[]>(args: T): void;
// T is readonly ["a", "b", "c"]
fnGood(["a", "b" ,"c"]);
同样,请记住修饰符 const
仅影响在调用中编写的对象、数组和原始表达式的推断,因此不会(或不能)修改的参数不会看到 as const
任何行为变化:
declare function fnGood<const T extends readonly string[]>(args: T): void;
const arr = ["a", "b" ,"c"];
// 'T' is still 'string[]'-- the 'const' modifier has no effect here
fnGood(arr);
有关更多详细信息,请参阅拉取请求和(第一个和第二个)激励问题。
extends
支持多个配置文件
tsconfig.json
管理多个项目时,拥有一个其他文件可以扩展的“基本”配置文件会很有帮助。这就是为什么 TypeScript 支持 extends
从 compilerOptions
.
// packages/front-end/src/tsconfig.json
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "../lib",
// ...
}
}
但是,在某些情况下,您可能希望从多个配置文件进行扩展。例如,假设使用发送到 npm 的 TypeScript 基本配置文件。如果您希望所有项目也使用 npm 包中的选项 @tsconfig/strictest
,那么有一个简单的解决方案:扩展 tsconfig.base.json
自@tsconfig/strictest
:
// tsconfig.base.json
{
"extends": "@tsconfig/strictest/tsconfig.json",
"compilerOptions": {
// ...
}
}
这在一定程度上起作用。如果您有任何项目不想使用 @tsconfig/strictest
,他们必须手动禁用这些选项,或者创建一个短度的 tsconfig.base.json
版本,该版本不扩展 @tsconfig/strictest
为了在此处提供更多灵活性,Typescript 5.0 现在允许该 extends
字段采用多个条目。例如,在这个配置文件中:
{
"extends": ["a", "b", "c"],
"compilerOptions": {
// ...
}
}
写这个有点像 c
直接扩展,其中 c
extends b
和 b
extends a
。如果任何字段“冲突”,则后一个条目获胜。
所以在下面的例子中, 和 strictNullChecks
都 noImplicitAny
在最终的 tsconfig.json
.
// tsconfig1.json
{
"compilerOptions": {
"strictNullChecks": true
}
}
// tsconfig2.json
{
"compilerOptions": {
"noImplicitAny": true
}
}
// tsconfig.json
{
"extends": ["./tsconfig1.json", "./tsconfig2.json"],
"files": ["./index.ts"]
}
再举一个例子,我们可以用下面的方式重写我们原来的例子。
// packages/front-end/src/tsconfig.json
{
"extends": ["@tsconfig/strictest/tsconfig.json", "../../../tsconfig.base.json"],
"compilerOptions": {
"outDir": "../lib",
// ...
}
}
有关更多详细信息,请阅读有关原始拉取请求的更多信息。
所有枚举都是联合枚举
当 TypeScript 最初引入枚举时,它们只不过是一组具有相同类型的数字常量。
enum E {
Foo = 10,
Bar = 20,
}
E.Foo
和 E.Bar
的唯一特别之处在于它们可以赋值给除 E
类型之外的任何类型。除此之外,他们几乎只是 number
s。
function takeValue(e: E) {}
takeValue(E.Foo); // works
takeValue(123); // error!
直到 TypeScript 2.0 引入了枚举文字类型,枚举才变得更加特殊。枚举文字类型为每个枚举成员提供了自己的类型,并将枚举本身变成了每个成员类型的联合。它们还允许我们仅引用枚举类型的一个子集,并缩小这些类型的范围。
// Color is like a union of Red | Orange | Yellow | Green | Blue | Violet
enum Color {
Red, Orange, Yellow, Green, Blue, /* Indigo */, Violet
}
// Each enum member has its own type that we can refer to!
type PrimaryColor = Color.Red | Color.Green | Color.Blue;
function isPrimaryColor(c: Color): c is PrimaryColor {
// Narrowing literal types can catch bugs.
// TypeScript will error here because
// we'll end up comparing 'Color.Red' to 'Color.Green'.
// We meant to use ||, but accidentally wrote &&.
return c === Color.Red && c === Color.Green && c === Color.Blue;
}
为每个枚举成员赋予其自己的类型的一个问题是,这些类型在某些部分与成员的实际值相关联。在某些情况下,无法计算该值——例如,枚举成员可以通过函数调用进行初始化。
enum E {
Blah = Math.random()
}
每当 TypeScript 遇到这些问题时,它都会悄悄退出并使用旧的枚举策略。这意味着放弃联合和文字类型的所有优势。
TypeScript 5.0 通过为每个计算成员创建唯一类型,设法将所有枚举变成联合枚举。这意味着现在可以缩小所有枚举的范围,并将其成员也作为类型引用。
有关此更改的更多详细信息,您可以阅读 GitHub 上的详细信息。
--moduleResolution 新增 bundler 支持
TypeScript 4.7 在 --module
和 --moduleResolution
设置中引入了 node16
和 nodenext
选项。这些选项的目的是更好地模拟 Node.js 中 ECMAScript 模块的精确查找规则;然而,这种模式有很多限制,其他工具并没有真正强制执行。
例如,在 Node.js 的 ECMAScript 模块中,任何相对导入都需要包含文件扩展名。
// entry.mjs
import * as utils from "./utils"; // wrong - we need to include the file extension.
import * as utils from "./utils.mjs"; // works
在 Node.js 和浏览器中这样做有一定的原因——它使文件查找更快,并且更适合原始文件服务器。但是对于许多使用捆绑器等工具的开发人员来说,node16
/nodenext
设置很麻烦,因为捆绑器没有这些限制中的大部分。在某些方面,node
解析模式对任何使用捆绑器的人来说都更好。
但在某些方面,原有的 node
解决模式已经落伍了。大多数现代捆绑器在 Node.js 中使用 ECMAScript 模块和 CommonJS 查找规则的融合。例如,无扩展名的导入就像在 CommonJS 中一样工作得很好,但是在查看包的 export
条件时,他们会更喜欢 ECMAScript 文件中的 import
条件。
为了模拟打包器的工作方式,TypeScript 现在引入了一种新策略:--moduleResolution bundler
。
{
"compilerOptions": {
"target": "esnext",
"moduleResolution": "bundler"
}
}
如果您正在使用像 Vite、esbuild、swc、Webpack、Parcel 和其他实施混合查找策略的现代捆绑器,那么新选项 bundler
应该非常适合您。
另一方面,如果您正在编写一个打算在 npm 上发布的库,则使用该 bundler
选项可以隐藏兼容性问题,这些问题可能会出现在您不使用捆绑器的用户身上。因此,在这些情况下,使用 node16
或 nodenext
解决方案可能是更好的途径。
要了解更多信息 --moduleResolution bundler
,请查看实施拉取请求。
自定义解析标志
JavaScript 工具现在可以模拟“混合”解析规则,就像 bundler
我们上面描述的模式一样。由于工具的支持可能略有不同,TypeScript 5.0 提供了启用或禁用一些功能的方法,这些功能可能适用于您的配置,也可能不适用于您的配置。
allowImportingTsExtensions
--allowImportingTsExtensions
允许使用特定于 TypeScript 的扩展名(如.ts
、.mts
或.tsx
)。
此标志仅在 --noEmit
或 --emitDeclarationOnly
启用时才被允许,因为这些导入路径在运行时无法在 JavaScript 输出文件中解析。这里的期望是您的解析器(例如您的捆绑器、运行时或其他一些工具)将使这些 .ts
文件之间的导入工作。
resolvePackageJsonExports
--resolvePackageJsonExports
强制 TypeScript 解析 package.json
的 exports
字段,如果曾经从 node_modules
中的读取过json文件。
当配置项 --modulerresolve
为 node16
、nodenext
和 bundler
时,该选项默认为true。
resolvePackageJsonImports
--resolvePackageJsonImports
强制 TypeScript 在从其祖先目录包含 package.json 的文件执行以 # 开头的查找时查询 package.json 文件的导入字段。
当配置项 --modulerresolve
为 node16
、nodenext
和 bundler
时,该选项默认为true。
allowArbitraryExtensions
在 TypeScript 5.0 中,当导入路径以不是已知 JavaScript 或 TypeScript 文件扩展名的扩展名结尾时,编译器将以 {file basename}.d.{extension}.ts
。 例如,如果您在捆绑项目中使用 CSS 加载器,您可能希望为这些样式表编写(或生成)声明文件:
/* app.css */
.cookie-banner {
display: none;
}
// app.d.css.ts
declare const css: {
cookieBanner: string;
};
export default css;
// App.tsx
import styles from "./app.css";
styles.cookieBanner; // string
默认情况下,此导入会引发错误,让您知道 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
或 imports
字段解析时,这些条件将添加到解析器默认使用的任何现有条件中。
例如,当在 tsconfig.json
中设置此字段时:
{
"compilerOptions": {
"target": "es2022",
"moduleResolution": "bundler",
"customConditions": ["my-condition"]
}
}
任何时候在 package.json
中引用 exports
或 imports
字段时,TypeScript 都会考虑调用 my-condition
的条件。
因此,当从具有以下内容的包中导入时 package.json
{
// ...
"exports": {
".": {
"my-condition": "./foo.mjs",
"node": "./bar.mjs",
"import": "./baz.mjs",
"require": "./biz.mjs"
}
}
}
TypeScript 将尝试查找与 foo.mjs
对应的文件。
该字段仅在 --moduleResolution
的 node16、nodenext 和 bundler 选项下有效
--verbatimModuleSyntax
默认情况下,TypeScript 会做一些叫做 import elision 的事情。基本上,如果你写类似
import { Car } from "./car";
export function drive(car: Car) {
// ...
}
TypeScript 检测到您只对类型使用导入,所以输出结果会将此导入代码删除。您的输出 JavaScript 可能看起来像这样:
export function drive(car) {
// ...
}
大多数时候这很好,因为如果 Car
不是从 ./car
导出的值,我们将收到运行时错误。
但它确实为某些边缘情况增加了一层复杂性。例如,请注意没有像这样的语句 import "./car";
——导入被完全删除。这实际上对有无副作用的模块产生影响。
TypeScript 针对 JavaScript 的 emit 策略还有另外几层复杂性——导入省略并不总是由导入的使用方式驱动——它通常还会参考值的声明方式。所以并不总是很清楚是否像下面这样的代码
export { Car } from "./car";
应该保留或丢弃。如果 Car
用 class
之类的东西声明,那么它可以保存在生成的 JavaScript 文件中。但如果 Car
仅声明为 type
别名或 interface
,则 JavaScript 文件 Car
根本不应导出。
虽然 TypeScript 可能能够根据来自跨文件的信息做出这些发出决定,但并非每个编译器都可以。
imports 和 exports 的修饰符 type
对这些情况有点帮助。我们可以明确指出导入或导出是否仅用于类型分析,并且可以通过使用修饰符将其完全删除到 JavaScript 文件中 type
。
// This statement can be dropped entirely in JS output
import type * as car from "./car";
// The named import/export 'Car' can be dropped in JS output
import { type Car } from "./car";
export { type Car } from "./car";
type
修饰符本身并不是很有用——默认情况下,模块省略仍然会删除导入,并且没有什么强制您区分 type
普通导入和导出。所以 TypeScript 有标志 --importsNotUsedAsValues
来确保你使用 type
修饰符,--preserveValueImports
以防止某些模块省略行为,并 --isolatedModules
确保你的 TypeScript 代码适用于不同的编译器。不幸的是,很难理解这 3 个标志的细节,并且仍然存在一些具有意外行为的边缘情况。
--verbatimModuleSyntax
TypeScript 5.0 引入了一个名为简化情况的新选项。规则要简单得多——任何没有 type
修饰符的导入或导出都会被保留。任何使用 type
修饰符的东西都会被完全丢弃。
// Erased away entirely.
import type { A } from "a";
// Rewritten to 'import { b } from "bcd";'
import { b, type c, type d } from "bcd";
// Rewritten to 'import {} from "xyz";'
import { type xyz } from "xyz";
有了这个新选项,所见即所得。
不过,当涉及到模块互操作时,这确实有一些影响。在此标志下,当您的设置或文件扩展名暗示不同的模块系统时,ECMAScript import
s 和 export
s 不会被重写为 require
调用。相反,你会得到一个错误。如果您需要发出使用 require
和 module.exports
的代码,则必须使用早于 ES2015 的 TypeScript 模块语法:
输入TypeScript | 输出 JavaScript |
---|---|
import foo = require("foo"); | const foo = require("foo"); |
function foo() {} function bar() {} function baz() {} export = { foo, bar, baz }; | function foo() {} function bar() {} function baz() {} module.exports = { foo, bar, baz }; |
虽然这是一个限制,但它确实有助于使一些问题更加明显。例如,忘记在 package.json
中设置 type 字段 是很常见的。--module node16
。因此,开发人员会在没有意识到的情况下开始编写 CommonJS 模块而不是 ES 模块,从而提供令人惊讶的查找规则和 JavaScript 输出。这个新标志确保您有意使用您正在使用的文件类型,因为语法是有意不同的。
因为 --verbatimModuleSyntax
提供了比 --importsNotUsedAsValues
和 --preserveValueImports
更一致的故事,所以这两个现有的标志已被弃用。
支持 export type *
当 TypeScript 3.8 引入纯类型导入时,新语法不允许用于 export * from "module"
或 export * as ns from "module"
重新导出。TypeScript 5.0 添加了对这两种形式的支持:
// models/vehicles.ts
export class Spaceship {
// ...
}
// models/index.ts
export type * as vehicles from "./vehicles";
// main.ts
import { vehicles } from "./models";
function takeASpaceship(s: vehicles.Spaceship) {
// ok - `vehicles` only used in a type position
}
function makeASpaceship() {
return new vehicles.Spaceship();
// ^^^^^^^^
// 'vehicles' cannot be used as a value because it was exported using 'export type'.
}
您可以在此处阅读有关实施的更多信息。
JSDoc 支持 @satisfies
TypeScript 4.9 引入了 satisfies
运算符。它确保表达式的类型兼容,而不影响类型本身。例如,让我们看下面的代码:
interface CompilerOptions {
strict?: boolean;
outDir?: string;
// ...
}
interface ConfigSettings {
compilerOptions?: CompilerOptions;
extends?: string | string[];
// ...
}
let myConfigSettings = {
compilerOptions: {
strict: true,
outDir: "../lib",
// ...
},
extends: [
"@tsconfig/strictest/tsconfig.json",
"../../../tsconfig.base.json"
],
} satisfies ConfigSettings;
在这里,TypeScript 知道它 myConfigSettings.extends
是用数组声明的——因为在 satisfies
验证我们对象的类型时,它并没有直接将其更改为 ConfigSettings
并丢失信息。所以如果我们想映射过来 extends
,那很好。
declare function resolveConfig(configPath: string): CompilerOptions;
let inheritedConfigs = myConfigSettings.extends.map(resolveConfig);
这对 TypeScript 用户很有帮助,但是很多人使用 TypeScript 来使用 JSDoc 注释对他们的 JavaScript 代码进行类型检查。这就是为什么 TypeScript 5.0 支持一个名为 JSDoc 的新标签,@satisfies
它做的事情完全一样。
/** @satisfies */
可以捕获类型不匹配:
// @ts-check
/**
* @typedef CompilerOptions
* @prop {boolean} [strict]
* @prop {string} [outDir]
*/
/**
* @satisfies {CompilerOptions}
*/
let myCompilerOptions = {
outdir: "../lib",
// ~~~~~~ oops! we meant outDir
};
但它会保留我们表达式的原始类型,允许我们稍后在代码中更精确地使用我们的值。
// @ts-check
/**
* @typedef CompilerOptions
* @prop {boolean} [strict]
* @prop {string} [outDir]
*/
/**
* @typedef ConfigSettings
* @prop {CompilerOptions} [compilerOptions]
* @prop {string | string[]} [extends]
*/
/**
* @satisfies {ConfigSettings}
*/
let myConfigSettings = {
compilerOptions: {
strict: true,
outDir: "../lib",
},
extends: [
"@tsconfig/strictest/tsconfig.json",
"../../../tsconfig.base.json"
],
};
let inheritedConfigs = myConfigSettings.extends.map(resolveConfig);
/** @satisfies */
也可以在任何带括号的表达式上内联使用。我们可以 myConfigSettings
这样写:
let myConfigSettings = /** @satisfies {ConfigSettings} */ ({
compilerOptions: {
strict: true,
outDir: "../lib",
},
extends: [
"@tsconfig/strictest/tsconfig.json",
"../../../tsconfig.base.json"
],
});
为什么?好吧,当您更深入地了解其他一些代码(例如函数调用)时,它通常更有意义。
compileCode(/** @satisfies {ConfigSettings} */ ({
// ...
}));
JSDoc 支持 @overload
在 TypeScript 中,您可以为函数指定重载。重载为我们提供了一种方式,可以用不同的参数调用一个函数,并可能返回不同的结果。他们可以限制调用者实际使用我们函数的方式,并优化他们将返回的结果。
// Our overloads:
function printValue(str: string): void;
function printValue(num: number, maxFractionDigits?: number): void;
// Our implementation:
function printValue(value: string | number, maximumFractionDigits?: number) {
if (typeof value === "number") {
const formatter = Intl.NumberFormat("en-US", {
maximumFractionDigits,
});
value = formatter.format(value);
}
console.log(value);
}
在这里,我们说过 printValue
将 string
或 number
作为其第一个参数。如果它需要一个 number
,它可以使用第二个参数来确定我们可以打印多少个小数位。
TypeScript 5.0 现在允许 JSDoc 使用新标签声明重载 @overload
。每个带有标记的 JSDoc 注释都 @overload
被视为以下函数声明的不同重载。
// @ts-check
/**
* @overload
* @param {string} value
* @return {void}
*/
/**
* @overload
* @param {number} value
* @param {number} [maximumFractionDigits]
* @return {void}
*/
/**
* @param {string | number} value
* @param {number} [maximumFractionDigits]
*/
function printValue(value, maximumFractionDigits) {
if (typeof value === "number") {
const formatter = Intl.NumberFormat("en-US", {
maximumFractionDigits,
});
value = formatter.format(value);
}
console.log(value);
}
现在,无论我们是在 TypeScript 还是 JavaScript 文件中编写,TypeScript 都可以让我们知道我们是否错误地调用了我们的函数。
// all allowed
printValue("hello!");
printValue(123.45);
printValue(123.45, 2);
printValue("hello!", 123); // error!
这个新标签的实现要归功于Tomasz Lenarcik。
在tsc --build
时可以传入的新指令
TypeScript 现在允许在 --build
模式下传递以下指令
--declaration
--emitDeclarationOnly
--declarationMap
--sourceMap
--inlineSourceMap
这使得自定义构建的某些部分变得更加容易,您可能有不同的开发和生产构建。
例如,库的开发构建可能不需要生成声明文件,但生产构建需要。项目可以将声明发射配置为默认关闭,只需使用
tsc --build -p ./my-project-dir
一旦在内循环中完成迭代,“生产”构建就可以传递指令 --declaration
。
tsc --build -p ./my-project-dir --declaration
编辑器中不区分大小写的导入排序
在 Visual Studio 和 VS Code 等编辑器中,TypeScript 支持组织和排序导入和导出的体验。但是,对于列表何时“排序”,通常会有不同的解释。
例如,下面的导入列表是否排序?
import {
Toggle,
freeze,
toBoolean,
} from "./utils";
答案可能令人惊讶地是“视情况而定”。如果我们不关心区分大小写,那么这个列表显然没有排序。这封信 f
出现在 t
和之前 T
。
但在大多数编程语言中,排序默认是比较字符串的字节值。JavaScript 比较字符串的方式意味着 "Toggle"
总是在前面 "freeze"
,因为根据 ASCII 字符编码,大写字母在小写字母之前。所以从这个角度来看,导入列表是排序的。
TypeScript 之前考虑对导入列表进行排序,因为它正在进行基本的区分大小写的排序。对于喜欢不区分大小写排序的开发人员,或者使用像 ESLint 这样默认需要不区分大小写排序的工具的开发人员来说,这可能是一个令人沮丧的地方。
TypeScript 现在默认检测区分大小写。这意味着 TypeScript 和 ESLint 等工具通常不会就如何最好地对导入进行排序而相互“争吵”。
我们的团队也一直在试验进一步的排序策略,您可以在此处阅读有关内容。这些选项最终可能由编辑器配置。目前,它们仍然不稳定且处于试验阶段,您现在可以通过使用 typescript.unstable
JSON 选项中的条目在 VS Code 中选择加入它们。以下是您可以尝试的所有选项(设置为默认值):
{
"typescript.unstable": {
// Should sorting be case-sensitive? Can be:
// - true
// - false
// - "auto" (auto-detect)
"organizeImportsIgnoreCase": "auto",
// Should sorting be "ordinal" and use code points or consider Unicode rules? Can be:
// - "ordinal"
// - "unicode"
"organizeImportsCollation": "ordinal",
// Under `"organizeImportsCollation": "unicode"`,
// what is the current locale? Can be:
// - [any other locale code]
// - "auto" (use the editor's locale)
"organizeImportsLocale": "en",
// Under `"organizeImportsCollation": "unicode"`,
// should upper-case letters or lower-case letters come first? Can be:
// - false (locale-specific)
// - "upper"
// - "lower"
"organizeImportsCaseFirst": false,
// Under `"organizeImportsCollation": "unicode"`,
// do runs of numbers get compared numerically (i.e. "a1" < "a2" < "a100")? Can be:
// - true
// - false
"organizeImportsNumericCollation": true,
// Under `"organizeImportsCollation": "unicode"`,
// do letters with accent marks/diacritics get sorted distinctly
// from their "base" letter (i.e. is é different from e)? Can be
// - true
// - false
"organizeImportsAccentCollation": true
},
"javascript.unstable": {
// same options valid here...
},
}
您可以阅读有关自动检测和指定不区分大小写的原始工作的更多详细信息,然后是更广泛的选项集。
switch
/case
语法补足
在编写 switch
语句时,TypeScript 现在会检测被检查的值何时具有文字类型。如果是这样,它将提供一个完成每个未发现的脚手架 case
。
您可以在 GitHub 上查看实施细节。
速度、内存和包大小优化
TypeScript 5.0 在我们的代码结构、数据结构和算法实现中包含许多强大的变化。这些都意味着你的整个体验应该更快——不仅仅是运行 TypeScript,甚至安装它。
以下是我们相对于 TypeScript 4.9 在速度和大小方面取得的一些有趣的胜利。
设想 | 时间或大小相对于 TS 4.9 |
---|---|
material-ui 构建时间 | 90% |
TypeScript 编译器启动时间 | 89% |
编剧建造时间 | 88% |
TypeScript Compiler 自建时间 | 87% |
Outlook Web 构建时间 | 82% |
VS 代码构建时间 | 80% |
打字稿 npm 包大小 | 59% |
如何?有一些显着的改进,我们希望在未来提供更多细节。但我们不会让您等待那篇博文。
首先,我们最近将 TypeScript 从命名空间迁移到模块,使我们能够利用现代构建工具来执行范围提升等优化。使用此工具、重新审视我们的打包策略并删除一些已弃用的代码,已将 TypeScript 4.9 的 63.8 MB 包大小减少了约 26.4 MB。它还通过直接函数调用为我们带来了显着的加速。我们在这里整理了一份关于我们迁移到模块的详细文章。
TypeScript 还为编译器中的内部对象类型增加了更多的统一性,并且还精简了存储在其中一些对象类型上的数据。这减少了多态操作,同时平衡了因使我们的对象形状更统一而增加的内存使用量。
在将信息序列化为字符串时,我们还执行了一些缓存。类型显示可能作为错误报告、声明发出、代码完成等的一部分发生,最终可能会相当昂贵。TypeScript 现在缓存了一些常用的机制以在这些操作中重用。
我们做出的另一个改进解析器的显着变化是利用 var
偶尔回避使用 let
和 const
跨闭包的成本。这提高了我们的一些解析性能。
总的来说,我们预计大多数代码库应该会看到 TypeScript 5.0 的速度提升,并且始终能够重现 10% 到 20% 之间的胜利。当然,这将取决于硬件和代码库特性,但我们鼓励您今天就在您的代码库上尝试一下!
有关详细信息,请参阅我们的一些显着优化:
重大更改和弃用
运行时要求
TypeScript 现在以 ECMAScript 2018 为目标。TypeScript 包还设置了最低预期引擎 12.20。对于 Node 用户,这意味着 TypeScript 5.0 的最低版本要求至少为 Node.js 12.20 及更高版本。
lib.d.ts
变化
更改 DOM 类型的生成方式可能会对现有代码产生影响。值得注意的是,某些属性已从 number
数字文字类型转换为数字文字类型,并且用于剪切、复制和粘贴事件处理的属性和方法已跨界面移动。
API 重大变更
在 TypeScript 5.0 中,我们转向了模块,删除了一些不必要的接口,并进行了一些正确性改进。有关更改内容的更多详细信息,请参阅我们的API 重大更改页面。
关系运算符中禁止的隐式强制转换
如果您编写的代码可能会导致隐式的字符串到数字强制转换,则 TypeScript 中的某些操作会警告您:
function func(ns: number | string) {
return ns * 4; // Error, possible implicit coercion
}
在 5.0 中,这也将应用于关系运算符>
、<
、<=
和>=
:
function func(ns: number | string) {
return ns > 4; // Now also an error
}
如果需要,要允许这样做,您可以显式地将操作数强制为number
using +
:
function func(ns: number | string) {
return +ns > 4; // OK
}
此正确性改进由 Mateusz Burzyński 提供。
枚举大修
enum
自从它的第一个版本以来,TypeScript 就一直存在一些关于 s 的奇怪之处。在 5.0 中,我们正在清理其中的一些问题,并减少理解 enum
您可以声明的各种 s 所需的概念数。
作为其中的一部分,您可能会看到两个主要的新错误。首先是将域外文字分配给类型 enum
现在会像人们预期的那样出错:
enum SomeEvenDigit {
Zero = 0,
Two = 2,
Four = 4
}
// Now correctly an error
let m: SomeEvenDigit = 1;
另一个是用混合数字和间接字符串枚举引用声明值的枚举会错误地创建一个全数字 enum
:
enum Letters {
A = "a"
}
enum Numbers {
one = 1,
two = Letters.A
}
// Now correctly an error
const t: number = Numbers.two;
您可以在相关更改中查看更多详细信息。
对构造函数中的参数装饰器进行更准确的类型检查--experimentalDecorators
TypeScript 5.0 使 --experimentalDecorators
。这一点变得明显的一个地方是在构造函数参数上使用装饰器时。
export declare const inject:
(entity: any) =>
(target: object, key: string | symbol, index?: number) => void;
export class Foo {}
export class C {
constructor(@inject(Foo) private x: any) {
}
}
此调用将失败,因为key
需要一个 string | symbol
,但构造函数参数收到一个键 undefined
。key
正确的解决方法是更改 within 的类型 inject
。如果您使用的是无法升级的库,一个合理的解决方法是包装 inject
一个类型更安全的装饰器函数,并在key
.
更多详情,请参阅本期。
弃用和默认更改
在 TypeScript 5.0 中,我们弃用了以下设置和设置值:
--target: ES3
--out
--noImplicitUseStrict
--keyofStringsOnly
--suppressExcessPropertyErrors
--suppressImplicitAnyIndexErrors
--noStrictGenericChecks
--charset
--importsNotUsedAsValues
--preserveValueImports
prepend
在项目参考中
在 TypeScript 5.5 之前,这些配置将继续被允许,届时它们将被完全删除,但是,如果您正在使用这些设置,您将收到警告。在 TypeScript 5.0 以及未来版本 5.1、5.2、5.3 和 5.4 中,您可以指定 "ignoreDeprecations": "5.0"
屏蔽这些警告提示。我们还将很快发布一个 4.9 补丁,以允许指定 ignoreDeprecations
以允许更平滑的升级。除了弃用之外,我们还更改了一些设置以更好地改进 TypeScript 中的跨平台行为。
--newLine
,它控制 JavaScript 文件中发出的行尾,如果未指定,过去常常根据当前操作系统进行推断。我们认为构建应该尽可能具有确定性,并且 Windows 记事本现在支持换行符行结尾,因此新的默认设置是 LF
。 旧的特定于操作系统的推理行为不再可用。
--forceConsistentCasingInFileNames
, 这确保了项目中对同一文件名的所有引用都在大小写中达成一致,现在默认为 true
。 这有助于捕获在不区分大小写的文件系统上编写的代码的差异问题。
您可以留下反馈并查看有关 5.0 弃用跟踪问题的更多信息
下一步是什么?
不要操之过急,TypeScript 5.1 已经在开发中了,我们所有的计划都已经在 GitHub 上了。如果你跃跃欲试,我们鼓励你尝试 TypeScript 的每日构建版本或针对 VS Code 的 JavaScript 和 TypeScript Nightly 扩展!
当然,如果您选择只享受 TypeScript 的新稳定版,我们也不会感到受伤。我们希望 TypeScript 5.0 让每个人的编码更快、更有趣。
Happy Hacking!
– Daniel Rosenwasser 和 TypeScript 团队
转载自:https://juejin.cn/post/7219908995339173943