TypeScript 5.1

更轻松地隐式返回 undefined 返回函数

¥Easier Implicit Returns for undefined-Returning Functions

在 JavaScript 中,如果函数运行结束时没有遇到 return,则返回值 undefined

¥In JavaScript, if a function finishes running without hitting a return, it returns the value undefined.

ts
function foo() {
// no return
}
// x = undefined
let x = foo();

但是,在之前的 TypeScript 版本中,唯一可以完全没有返回语句的函数是返回 voidany 的函数。这意味着即使你明确指定“此函数返回 undefined”,你也必须至少有一个 return 语句。

¥However, in previous versions of TypeScript, the only functions that could have absolutely no return statements were void- and any-returning functions. That meant that even if you explicitly said “this function returns undefined” you were forced to have at least one return statement.

ts
// ✅ 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,这可能会很麻烦。 - 你需要至少有一个显式返回 undefinedreturn 的语句以及一个显式注解。

¥This could be a pain if some API expected a function returning undefined - you would need to have either at least one explicit return of undefined or a return statement and an explicit annotation.

ts
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 语句等,这似乎有点分散注意力。

¥This behavior was frustrating and confusing, especially when calling functions outside of one’s control. Understanding the interplay between inferring void over undefined, whether an undefined-returning function needs a return statement, etc. seems like a distraction.

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

¥First, TypeScript 5.1 now allows undefined-returning functions to have no return statement.

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

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

¥Second, if a function has no return expressions and is being passed to something expecting a function that returns undefined, TypeScript infers undefined for that function’s return type.

ts
// ✅ 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 结尾。

¥To address another similar pain-point, under TypeScript’s --noImplicitReturns option, functions returning only undefined now have a similar exception to void, in that not every single code path must end in an explicit return.

ts
// ✅ Works in TypeScript 5.1 under '--noImplicitReturns'!
function f(): undefined {
if (Math.random()) {
// do some stuff...
return;
}
}

更多信息,你可以阅读 原始问题实现拉取请求

¥For more information, you can read up on the original issue and the implementing pull request.

不相关 Getter 和 Setter 的类型

¥Unrelated Types for Getters and Setters

TypeScript 4.3 使得 getset 访问器对可以指定两种不同的类型成为可能。

¥TypeScript 4.3 made it possible to say that a get and set accessor pair might specify two different types.

ts
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 类型的子类型。这意味着写作

¥Initially we required that the get type had to be a subtype of the set type. This meant that writing

ts
box.value = box.value;

始终有效。

¥would always be valid.

但是,许多现有和拟议的 API 的 getter 和 setter 类型完全不相关。例如,考虑一个最常见的例子 - DOM 和 CSSStyleRule API 中的 style 属性。每个样式规则的 style 属性 类型都是 CSSStyleDeclaration 类型的;但是,如果你尝试写入该属性,则只有字符串才能正常工作!

¥However, there are plenty of existing and proposed APIs that have completely unrelated types between their getters and setters. For example, consider one of the most common examples - the style property in the DOM and CSSStyleRule API. Every style rule has a style property that is a CSSStyleDeclaration; however, if you try to write to that property, it will only work correctly with a string!

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

¥TypeScript 5.1 now allows completely unrelated types for get and set accessor properties, provided that they have explicit type annotations. And while this version of TypeScript does not yet change the types for these built-in interfaces, CSSStyleRule can now be defined in the following way:

ts
interface CSSStyleRule {
// ...
/** Always reads as a `CSSStyleDeclaration` */
get style(): CSSStyleDeclaration;
/** Can only write a `string` here. */
set style(newValue: string);
// ...
}

这还允许其他模式,例如要求 set 访问器仅接受 “valid” 数据,但指定 get 访问器在某些底层状态尚未初始化时可以返回 undefined

¥This also allows other patterns like requiring set accessors to accept only “valid” data, but specifying that get accessors may return undefined if some underlying state hasn’t been initialized yet.

ts
class SafeBox {
#value: string | undefined;
// Only accepts strings!
set value(newValue: string) {
}
// Must check for 'undefined'!
get value(): string | undefined {
return this.#value;
}
}

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

¥In fact, this is similar to how optional properties are checked under --exactOptionalProperties.

你可以阅读有关 实现拉取请求 的更多信息。

¥You can read up more on the implementing pull request.

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

¥Decoupled Type-Checking Between JSX Elements and JSX Tag Types

TypeScript 使用 JSX 的一个痛点是它对每个 JSX 元素标签的类型有要求。

¥One pain point TypeScript had with JSX was its requirements on the type of every JSX element’s tag.

对于上下文,JSX 元素可以是以下任一项:

¥For context, a JSX element is either of the following:

tsx
// A self-closing JSX tag
<Foo />
// A regular element with an opening/closing tag
<Bar></Bar>

在对 <Foo /><Bar></Bar> 进行类型检查时,TypeScript 总是查找名为 JSX 的命名空间,并从中获取名为 Element 的类型。 - 或者更直接地说,它会查找 JSX.Element

¥When type-checking <Foo /> or <Bar></Bar>, TypeScript always looks up a namespace called JSX and fetches a type out of it called Element - or more directly, it looks up JSX.Element.

但是,为了检查 FooBar 本身是否可以用作标签名,TypeScript 会粗略地获取 FooBar 返回或构造的类型,并检查其与 JSX.Element(或另一种名为 JSX.ElementClass 的类型,如果该类型可构造)的兼容性。

¥But to check whether Foo or Bar themselves were valid to use as tag names, TypeScript would roughly just grab the types returned or constructed by Foo or Bar and check for compatibility with JSX.Element (or another type called JSX.ElementClass if the type is constructable).

此处的限制意味着,如果组件返回或 “rendered” 的类型比 JSX.Element 更宽泛,则无法使用。例如,JSX 库可能允许组件返回 stringPromise

¥The limitations here meant that components could not be used if they returned or “rendered” a more broad type than just JSX.Element. For example, a JSX library might be fine with a component returning strings or Promises.

举一个更具体的例子,React 正在考虑为返回 Promise 的组件添加有限的支持,但现有版本的 TypeScript 无法表达这一点,除非有人大幅放宽 JSX.Element 的类型。

¥As a more concrete example, React is considering adding limited support for components that return Promises, but existing versions of TypeScript cannot express that without someone drastically loosening the type of JSX.Element.

tsx
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 元素中用作标签的有效内容。因此,现在它的类型可能类似于

¥To provide libraries with a way to express this, TypeScript 5.1 now looks up a type called JSX.ElementType. ElementType specifies precisely what is valid to use as a tag in a JSX element. So it might be typed today as something like

tsx
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 IntrinsicAttributes extends /*...*/ {}
export type Element = /*...*/;
export type ElementClass = /*...*/;
}

我们要感谢 Sebastian Silbermann 贡献了 此项变更

¥We’d like to extend our thanks to Sebastian Silbermann who contributed this change!

命名空间 JSX 属性

¥Namespaced JSX Attributes

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

¥TypeScript now supports namespaced attribute names when using JSX.

tsx
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 上以类似的方式查找命名空间标签名称。

¥Namespaced tag names are looked up in a similar way on JSX.IntrinsicAttributes when the first segment of the name is a lowercase name.

tsx
// 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 提供!

¥This contribution was provided thanks to Oleksandr Tarasiuk.

typeRoots 在模块解析中被参考

¥typeRoots Are Consulted In Module Resolution

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

¥When TypeScript’s specified module lookup strategy is unable to resolve a path, it will now resolve packages relative to the specified typeRoots.

有关详细信息,请参阅 此拉取请求

¥See this pull request for more details.

将声明移至现有文件

¥Move Declarations to Existing Files

除了将声明移动到新文件之外,TypeScript 现在还提供了一个预览功能,用于将声明移动到现有文件。你可以在最新版本的 Visual Studio Code 中尝试此功能。

¥In addition to moving declarations to new files, TypeScript now ships a preview feature for moving declarations to existing files as well. You can try this functionality out in a recent version of Visual Studio Code.

Moving a function 'getThanks' to an existing file in the workspace.

请记住,此功能目前处于预览阶段,我们正在寻求进一步的反馈。

¥Keep in mind that this feature is currently in preview, and we are seeking further feedback on it.

https://github.com/microsoft/TypeScript/pull/53542

JSX 标签的链接游标

¥Linked Cursors for JSX Tags

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

¥TypeScript now supports linked editing for JSX tag names. Linked editing (occasionally called “mirrored cursors”) allows an editor to edit multiple locations at the same time automatically.

An example of JSX tags with linked editing modifying a JSX fragment and a div element.

此新功能应在 TypeScript 和 JavaScript 文件中都有效,并且可以在 Visual Studio Code Insiders 中启用。在 Visual Studio Code 中,你可以在“设置”UI 中编辑 Editor: Linked Editing 选项:

¥This new feature should work in both TypeScript and JavaScript files, and can be enabled in Visual Studio Code Insiders. In Visual Studio Code, you can either edit the Editor: Linked Editing option in the Settings UI:

Visual Studio Code's Editor: Linked Editing` option

或者在你的 JSON 设置文件中配置 editor.linkedEditing

¥or configure editor.linkedEditing in your JSON settings file:

jsonc
{
// ...
"editor.linkedEditing": true,
}

Visual Studio 17.7 Preview 1 也将支持此功能。

¥This feature will also be supported by Visual Studio 17.7 Preview 1.

你可以在这里查看 我们对链接编辑的实现

¥You can take a look at our implementation of linked editing here!

@param JSDoc 标签的代码片段补全

¥Snippet Completions for @param JSDoc Tags

在 TypeScript 和 JavaScript 文件中输入 @param 标签时,TypeScript 现在都提供代码片段补全功能。这可以帮助减少在 JavaScript 中记录代码或添加 JSDoc 类型时的一些输入和文本跳转。

¥TypeScript now provides snippet completions when typing out a @param tag in both TypeScript and JavaScript files. This can help cut down on some typing and jumping around text as you document your code or add JSDoc types in JavaScript.

An example of completing JSDoc param comments on an 'add' function.

你可以 查看此新功能在 GitHub 上的实现方式

¥You can check out how this new feature was implemented on GitHub.

优化

¥Optimizations

避免不必要的类型实例化

¥Avoiding Unnecessary Type Instantiation

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

¥TypeScript 5.1 now avoids performing type instantiation within object types that are known not to contain references to outer type parameters. This has the potential to cut down on many unnecessary computations, and reduced the type-checking time of material-ui’s docs directory by over 50%.

你可以 在 GitHub 上查看此变更所涉及的变更

¥You can see the changes involved for this change on GitHub.

联合字面量的否定大小写检查

¥Negative Case Checks for Union Literals

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

¥When checking if a source type is part of a union type, TypeScript will first do a fast look-up using an internal type identifier for that source. If that look-up fails, then TypeScript checks for compatibility against every type within the union.

将字面量类型与纯字面量类型的联合关联时,TypeScript 现在可以避免对联合中的所有其他类型进行完整遍历。此假设是安全的,因为 TypeScript 始终会驻留/缓存字面量类型。 - 但是,有一些与 “fresh” 字面量类型相关的边缘情况需要处理。

¥When relating a literal type to a union of purely literal types, TypeScript can now avoid that full walk against every other type in the union. This assumption is safe because TypeScript always interns/caches literal types - though there are some edge cases to handle relating to “fresh” literal types.

此优化 能够将 本期代码 的类型检查时间从大约 45 秒缩短到大约 0.4 秒。

¥This optimization was able to reduce the type-checking time of the code in this issue from about 45 seconds to about 0.4 seconds.

减少对 JSDoc 扫描器的调用解析

¥Reduced Calls into Scanner for JSDoc Parsing

旧版本的 TypeScript 解析 JSDoc 注释时,会使用扫描器/标记器将注释分解为细粒度的标记,然后将内容重新拼凑在一起。这可能有助于规范化注释文本,以便多个空格可以合并为一个;但是这非常 “chatty”,这意味着解析器和扫描器会频繁地来回跳转,增加了 JSDoc 解析的开销。

¥When older versions of TypeScript parsed out a JSDoc comment, they would use the scanner/tokenizer to break the comment into fine-grained tokens and piece the contents back together. This could be helpful for normalizing comment text, so that multiple spaces would just collapse into one; but it was extremely “chatty” and meant the parser and scanner would jump back and forth very often, adding overhead to JSDoc parsing.

TypeScript 5.1 已将更多逻辑转移到将 JSDoc 注释分解到扫描器/标记器中。扫描器现在会将更大的内容块直接返回给解析器,以便其根据需要执行操作。

¥TypeScript 5.1 has moved more logic around breaking down JSDoc comments into the scanner/tokenizer. The scanner now returns larger chunks of content directly to the parser to do as it needs.

这些变更 将几个 10MB 大小、主要由散文注释组成的 JavaScript 文件的解析时间缩短了约一半。为了更现实的例子,我们的性能套件的 xstate 快照减少了大约 300 毫秒的解析时间,从而加快了加载和分析速度。

¥These changes have brought down the parse time of several 10Mb mostly-prose-comment JavaScript files by about half. For a more realistic example, our performance suite’s snapshot of xstate dropped about 300ms of parse time, making it faster to load and analyze.

打破变更

¥Breaking Changes

最低运行时要求:ES2020 和 Node.js 14.17

¥ES2020 and Node.js 14.17 as Minimum Runtime Requirements

TypeScript 5.1 现在支持 ECMAScript 2020 中引入的 JavaScript 功能。因此,TypeScript 至少必须在一个相当现代的运行时中运行。对于大多数用户来说,这意味着 TypeScript 现在只能在 Node.js 14.17 及更高版本上运行。

¥TypeScript 5.1 now ships JavaScript functionality that was introduced in ECMAScript 2020. As a result, at minimum TypeScript must be run in a reasonably modern runtime. For most users, this means TypeScript now only runs on Node.js 14.17 and later.

如果你尝试在旧版本的 Node.js(例如 Node 10 或 12)下运行 TypeScript 5.1,则在运行 tsc.jstsserver.js 时可能会看到类似以下错误:

¥If you try running TypeScript 5.1 under an older version of Node.js such as Node 10 or 12, you may see an error like the following from running either tsc.js or tsserver.js:

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,你会收到类似以下来自 npm 的错误消息:

¥Additionally, if you try installing TypeScript you’ll get something like the following error messages from npm:

npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE package: 'typescript@5.1.1-rc',
npm WARN EBADENGINE required: { node: '>=14.17' },
npm WARN EBADENGINE current: { node: 'v12.22.12', npm: '8.19.2' }
npm WARN EBADENGINE }

来自 Yarn:

¥from Yarn:

error typescript@5.1.1-rc: The engine "node" is incompatible with this module. Expected version ">=14.17". Got "12.22.12"
error Found incompatible module.

查看此处有关此变更的更多信息

¥See more information around this change here.

显式 typeRoots 禁用 node_modules/@types 的向上遍历

¥Explicit typeRoots Disables Upward Walks for node_modules/@types

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

¥Previously, when the typeRoots option was specified in a tsconfig.json but resolution to any typeRoots directories had failed, TypeScript would still continue walking up parent directories, trying to resolve packages within each parent’s node_modules/@types folder.

此行为可能导致过度查找,已在 TypeScript 5.1 中禁用。因此,你可能会根据 tsconfig.jsontypes 选项或 /// <reference > 指令中的条目看到类似以下的错误。

¥This behavior could prompt excessive look-ups and has been disabled in TypeScript 5.1. As a result, you may begin to see errors like the following based on entries in your tsconfig.json’s types option or /// <reference > directives

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"'.

解决方案通常是将 node_modules/@types 的特定条目添加到你的 typeRoots 中:

¥The solution is typically to add specific entries for node_modules/@types to your typeRoots:

jsonc
{
"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",
]
}
}

更多信息请参见 关于我们问题跟踪器上的原始变更

¥More information is available on the original change on our issue tracker.