TypeScript 5.1发布

今天,我们非常激动地宣布发布 TypeScript 5.1!

如果您还不熟悉 TypeScript,它是在 JavaScript 基础上添加了一些称为类型的结构的语言。这些类型可以描述程序的某些细节,并且在编译之前,TypeScript 可以对它们进行检查,以捕捉可能的拼写错误、逻辑错误等问题。TypeScript 还使用这些类型来提供编辑器工具,如代码补全、重构等。实际上,如果您已经在 Visual Studio 或 VS Code 等编辑器中编写 JavaScript,那么您的体验已经得到了 TypeScript 的增强!您可以在 https://typescriptlang.org/ 上了解更多信息。

要开始使用 TypeScript,您可以通过 NuGet 获取它,或者更常见地通过 npm 使用以下命令获取:

1
npm install -D typescript

更新列表

自从 Beta 版本和 RC 版本以后有什么新内容?

自从 Beta 版本以后,我们根据调整后的提议,对修饰器中的初始化钩子行为进行了一些更正。我们还对在 isolatedModules 下的发射行为进行了更改,确保脚本文件不会被重写为模块。这也意味着,使用 transpileModule API 时,也会确保脚本文件不被解释为模块,因为它假设使用了 isolatedModules。

自从 RC 版本以后,我们对将声明移动到现有文件的内置重构进行了微小的迭代;然而,我们认为实现仍然需要一些改进。因此,您可能目前无法在大多数编辑器中访问它,只能通过使用 TypeScript 的夜间版本选择启用。我们预计 TypeScript 5.2 或将来的 TypeScript 5.1 补丁版本将重新引入此重构功能。

##更容易实现未定义返回的函数的隐式返回
在 JavaScript 中,如果一个函数在执行过程中没有遇到 return 语句,那么它将返回 undefined 值。

1
2
3
4
5
6
function foo() {
// no return
}

// x = undefined
let x = foo();

然而,在之前的 TypeScript 版本中,唯一可以完全没有返回语句的函数是 void 类型和任意返回类型的函数。这意味着,即使你明确地声明了”这个函数返回 undefined”,你仍然被迫至少有一个 return 语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ✅ fine - we inferred that 'f1' returns 'void'
function f1() {
// no returns
}

// ✅ fine - 'void' doesn't need a return statement
function f2(): void {
// no returns
}

// ✅ fine - 'any' doesn't need a return statement
function f3(): any {
// no returns
}

// ❌ error!
// A function whose declared type is neither 'void' nor 'any' must return a value.
function f4(): undefined {
// no returns
}

这可能会带来一些麻烦,如果某个 API 期望一个返回 undefined 的函数,你要么需要至少有一个明确的返回 undefined 语句,要么需要一个 return 语句和一个明确的类型注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
declare function takesFunction(f: () => undefined): undefined;

// ❌ error!
// Argument of type '() => void' is not assignable to parameter of type '() => undefined'.
takesFunction(() => {
// no returns
});

// ❌ error!
// A function whose declared type is neither 'void' nor 'any' must return a value.
takesFunction((): undefined => {
// no returns
});

// ❌ error!
// Argument of type '() => void' is not assignable to parameter of type '() => undefined'.
takesFunction(() => {
return;
});

// ✅ works
takesFunction(() => {
return undefined;
});

// ✅ works
takesFunction((): undefined => {
return;
});

这种行为令人沮丧和困惑,特别是在调用无法控制的函数时。理解推断 void 和 undefined 之间的相互作用,以及一个返回 undefined 的函数是否需要一个 return 语句等,似乎是一个分散注意力的因素。

首先,TypeScript 5.1 现在允许返回 undefined 的函数没有 return 语句。

1
2
3
4
5
6
7
8
9
10
// ✅ Works in TypeScript 5.1!
function f4(): undefined {
// no returns
}

// ✅ Works in TypeScript 5.1!
takesFunction((): undefined => {
// no returns
});

其次,如果一个函数没有返回表达式,并且被传递给期望返回 undefined 的函数的地方,TypeScript 推断该函数的返回类型为 undefined。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ✅ Works in TypeScript 5.1!
takesFunction(function f() {
// ^ return type is undefined

// no returns
});

// ✅ Works in TypeScript 5.1!
takesFunction(function f() {
// ^ return type is undefined

return;
});

为了解决另一个类似的痛点,在 TypeScript 的 –noImplicitReturns 选项下,只返回 undefined 的函数现在有了类似于 void 的例外情况,即不是每条代码路径都必须以明确的 return 结束。

1
2
3
4
5
6
7
8
// ✅ Works in TypeScript 5.1 under '--noImplicitReturns'!
function f(): undefined {
if (Math.random()) {
// do some stuff...
return;
}
}

若要了解更多信息,您可以阅读issue代码变更

不相关类型getter 和 setter 的支持

TypeScript 4.3 版本使得可以将一个 get 和 set 访问器对指定为两种不同的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
interface Serializer {
set value(v: string | number | boolean);
get value(): string;
}

declare let box: Serializer;

// Allows writing a 'boolean'
box.value = true;

// Comes out as a 'string'
console.log(box.value.toUpperCase());

最初,我们要求 get 类型必须是 set 类型的子类型。这意味着编写以下代码时是合理的:

1
2
box.value = box.value;

然而,存在许多现有的和提议中的 API,在它们的 getter 和 setter 之间具有完全不相关的类型。例如,考虑最常见的例子之一——DOM 中的 style 属性和 CSSStyleRule API。每个样式规则都有一个 style 属性,它是一个 CSSStyleDeclaration 类型;然而,如果您尝试对该属性进行赋值,它只能使用字符串进行正确的赋值!

TypeScript 5.1 现在允许完全不相关的类型用于 getset 访问器属性,前提是它们具有显式的类型注解。虽然 TypeScript 的这个版本尚未更改这些内置接口的类型,但可以如下定义 CSSStyleRule

1
2
3
4
5
6
7
8
9
10
11
12
interface CSSStyleRule {
// ...

/** Always reads as a `CSSStyleDeclaration` */
get style(): CSSStyleDeclaration;

/** Can only write a `string` here. */
set style(newValue: string);

// ...
}

这也允许其他模式,例如要求 set 访问器只接受“有效”的数据,但指定 get 访问器可以在某些基础状态尚未初始化时返回 undefined。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class SafeBox {
#value: string | undefined;

// Only accepts strings!
set value(newValue: string) {

}

// Must check for 'undefined'!
get value(): string | undefined {
return this.#value;
}
}

实际上,这与在 –exactOptionalProperties 下检查可选属性的方式类似。

更多信息 变更信息.

在 JSX 元素和 JSX 标签类型之间解耦的类型检查

TypeScript 在处理 JSX 时的一个痛点是对每个 JSX 元素标签类型的要求。而 TypeScript 的这个版本使得 JSX 库能够更准确地描述 JSX 组件的返回类型。对于很多人来说,这具体意味着可以在 React 中使用异步服务器组件。

为了提供一些背景和上下文,JSX 元素可以是以下之一:

1
2
3
4
5
6
// A self-closing JSX tag
<Foo />

// A regular element with an opening/closing tag
<Bar></Bar>

当对 <Foo /><Bar></Bar> 进行类型检查时,TypeScript 总是会查找名为 JSX 的命名空间,并获取名为 Element 的类型。换句话说,它会寻找 JSX.Element

但是,为了检查 FooBar 本身是否是有效的标签名称,TypeScript 大致上会获取由 FooBar 返回或构造的类型,并检查其是否与 JSX.Element(或另一个名为 JSX.ElementClass 的类型,如果该类型可以被构造)兼容。

这里的限制意味着如果组件返回或者”渲染”的类型比仅仅是 JSX.Element 更广泛,那么这些组件就不能被使用。例如,一个 JSX 库可能允许组件返回字符串或 Promise。

作为更具体的例子,React 的未来版本已经提议有限地支持返回 Promise 的组件,但是现有的 TypeScript 版本无法表达这一点,除非有人极大地放宽 JSX.Element 的类型。

1
2
3
4
5
6
7
8
9
10
11
import * as React from "react";

async function Foo() {
return <div></div>;
}

let element = <Foo />;
// ~~~
// 'Foo' cannot be used as a JSX component.
// Its return type 'Promise<Element>' is not a valid JSX element.

为了为库提供一种表达这一点的方式,TypeScript 5.1 现在查找名为 JSX.ElementType 的类型。ElementType 精确地指定在 JSX 元素中作为标签使用的有效内容。因此,它可能在当前的类型定义中被定义为类似以下的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace JSX {
export type ElementType =
// All the valid lowercase tags
keyof IntrinsicAttributes
// Function components
(props: any) => Element
// Class components
new (props: any) => ElementClass;

export interface IntrinsictAttributes extends /*...*/ {}
export type Element = /*...*/;
export type ClassElement = /*...*/;
}

感谢 Sebastian Silbermann 提供了 相关变更!

命名空间 JSX 属性

在使用 JSX 时,TypeScript 现在支持命名空间属性名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import * as React from "react";

// Both of these are equivalent:
const x = <Foo a:b="hello" />;
const y = <Foo a : b="hello" />;

interface FooProps {
"a:b": string;
}

function Foo(props: FooProps) {
return <div>{props["a:b"]}</div>;
}

当命名空间标签名的第一个段是小写名称时,JSX.IntrinsicAttributes 上的命名空间属性名会以类似的方式进行查找。

1
2
3
4
5
6
7
8
9
10
// In some library's code or in an augmentation of that library:
namespace JSX {
interface IntrinsicElements {
["a:b"]: { prop: string };
}
}

// In our code:
let x = <a:b prop="hello!" />;

感谢 Oleksandr Tarasiuk贡献

模块解析过程中将会考虑 typeRoots 配置

当 TypeScript 指定的模块查找策略无法解析路径时,它将会相对于指定的 typeRoots 解析包。

查看 变更 获取更多细节.

引入了 JSX 标签的 “Linked Cursors” 功能

TypeScript 现在支持 JSX 标签名称的链接编辑。链接编辑(有时称为 “镜像光标”)允许编辑器自动同时编辑多个位置。

这个新功能可以在 TypeScript 和 JavaScript 文件中使用,并且可以在 Visual Studio Code Insiders 中启用。在 Visual Studio Code 中,您可以通过编辑”Settings UI”中的”Editor: Linked Editing”选项来启用该功能:

image.png
或者在您的 JSON 设置文件中配置 editor.linkedEditing 选项:

1
2
3
4
5
{
// ...
"editor.linkedEditing": true,
}

Visual Studio 17.7 会支持这个功能.

@param 标签提供代码段补全功能

TypeScript 现在在 TypeScript 和 JavaScript 文件中为 @param 标签提供代码段补全功能。这可以帮助您在编写代码文档或在 JavaScript 中添加 JSDoc 类型时减少一些输入和跳转。


当您在 JSDoc 注释中输入 @param 时,TypeScript 将提供代码段补全的建议。这些代码段包含参数名称、类型和描述的占位符,您可以方便地选择和自定义。

通过这一功能,您无需手动输入和跳转文本,可以更高效地生成标准化的 @param 标签。这大大提升了使用 JSDoc 注释文档函数参数的准确性和一致性,节省了时间并减少了错误。

优化

避免不必要的类型实例化

TypeScript 5.1 现在避免在已知不包含对外部类型参数的引用的对象类型中进行类型实例化。这有潜力减少许多不必要的计算,并且将 material-ui 的文档目录的类型检查时间缩短了 50% 以上。

这个优化意味着在一些情况下,TypeScript 不会在对象类型中创建不必要的类型实例。它通过对类型的推断和分析进行改进来实现。当对象类型中没有引用外部类型参数时,TypeScript 将避免进行不必要的类型实例化。

通过减少不必要的类型实例化,TypeScript 5.1 在类型检查方面提供了更高的效率。尤其是在具有复杂类型层级或大型代码库的情况下,可以显著提高类型检查的性能。

这项优化使得编译时间更短,开发体验更高效。开发人员可以更轻松地处理更大的代码库和更复杂的项目,并获得更好的性能。

联合字面量的负面情况检查

在检查源类型是否属于联合类型时,TypeScript首先使用内部类型标识符对该源类型进行快速查找。如果查找失败,则TypeScript会对联合类型中的每个类型进行兼容性检查。

在将字面量类型与纯字面量类型的联合类型相关联时,TypeScript现在可以避免对联合类型中的每个其他类型进行完整遍历。这种假设是安全的,因为TypeScript始终对字面量类型进行内部缓存 - 尽管存在一些涉及“新鲜”字面量类型的边缘情况需要处理。

这种优化能够将代码中的类型检查时间从约45秒减少到约0.4秒。您可以在issue中看到这个优化的效果。

在解析 JSDoc 时,减少了对扫描器的调用次数

在较旧版本的 TypeScript 中,解析 JSDoc 注释时会使用扫描器/标记器将注释分解为细粒度的标记,并将内容重新组合。这在规范化注释文本方面可能是有帮助的,例如多个空格会被合并为一个空格,但这种方法会导致解析器和扫描器之间频繁的跳转,增加了 JSDoc 解析的开销。

TypeScript 5.1 对将 JSDoc 注释分解为扫描器/标记器中的内容进行了更多的逻辑调整。现在,扫描器直接将较大的内容块返回给解析器以供处理。

这些更改 使得解析几个大小约为10MB的主要由自然语言组成的 JavaScript 文件的时间减少了约一半。以更实际的例子来说,我们的性能测试套件对xstate的快照的解析时间减少了约300毫秒,使得加载和分析速度更快。

Breaking Changes

ES2020和Node.js 14.17作为最低运行时要求。

TypeScript 5.1 现在包含了在 ECMAScript 2020 中引入的 JavaScript 功能。因此,TypeScript 至少需要在一个相对较新的运行时环境下运行。对于大多数用户来说,这意味着 TypeScript 现在只能在 Node.js 14.17 及更高版本上运行。

如果你尝试在较旧版本的 Node.js(如 Node 10 或 12)上运行 TypeScript 5.1,可能会遇到以下错误,无论是运行 tsc.js 还是 tsserver.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
node_modules/typescript/lib/tsserver.js:2406
for (let i = startIndex ?? 0; i < array.length; i++) {
^

SyntaxError: Unexpected token '?'
at wrapSafe (internal/modules/cjs/loader.js:915:16)
at Module._compile (internal/modules/cjs/loader.js:963:27)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
at Module.load (internal/modules/cjs/loader.js:863:32)
at Function.Module._load (internal/modules/cjs/loader.js:708:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
at internal/main/run_main_module.js:17:47

此外,如果你尝试安装 TypeScript,你将会收到类似以下的错误信息:

1
2
3
4
5
6
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE package: 'typescript@5.1.3',
npm WARN EBADENGINE required: { node: '>=14.17' },
npm WARN EBADENGINE current: { node: 'v12.22.12', npm: '8.19.2' }
npm WARN EBADENGINE }

Yarn

1
2
3
error typescript@5.1.3: The engine "node" is incompatible with this module. Expected version ">=14.17". Got "12.22.12"
error Found incompatible module.

更多变更信息.

显式指定了 typeRoots 后,将禁用对 node_modules/@types 的向上查找

以前,在 tsconfig.json 中指定了 typeRoots 选项但无法解析到任何 typeRoots 目录时,TypeScript 仍然会继续向上遍历父目录,尝试在每个父目录的 node_modules/@types 文件夹中解析包。

这种行为可能导致过多的查找,并且在 TypeScript 5.1 中已被禁用。因此,您可能会开始看到类似以下错误的错误,这是基于您的 tsconfig.json 中的 types 选项或 /// 指令的条目而产生的:

1
2
3
4
5
6
error TS2688: Cannot find type definition file for 'node'.
error TS2688: Cannot find type definition file for 'mocha'.
error TS2688: Cannot find type definition file for 'jasmine'.
error TS2688: Cannot find type definition file for 'chai-http'.
error TS2688: Cannot find type definition file for 'webpack-env"'.

解决方案通常是在您的 typeRoots 中添加针对 node_modules/@types 的特定条目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"compilerOptions": {
"types": [
"node",
"mocha"
],
"typeRoots": [
// Keep whatever you had around before.
"./some-custom-types/",

// You might need your local 'node_modules/@types'.
"./node_modules/@types",

// You might also need to specify a shared 'node_modules/@types'
// if you're using a "monorepo" layout.
"../../node_modules/@types",
]
}
}

更多信息可以在我们的问题跟踪器中找到。