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”,你仍然被迫至少包含一个返回语句。

🌐 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 的函数,这可能会很麻烦——你需要至少有一次显式返回 undefined,或者有一个 return 语句 并且 需要显式注解。

🌐 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;
});

这种行为令人沮丧且令人困惑,尤其是在调用自己无法控制的函数时。理解在 voidundefined 之间做推断的相互作用,或者一个返回 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 的函数没有返回语句。

🌐 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 中的 style 属性和 CSSStyleRule API。每个样式规则都有 一个 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 访问器只接受“有效”数据,但规定 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>

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).

这里的限制意味着,如果组件返回或“渲染”的类型比仅仅 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 提供了此 贡献

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 中,你可以在设置界面中编辑 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 的 docs 目录 的类型检查时间减少了超过 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 总是对字面量类型进行内部处理/缓存,不过仍有一些与“新鲜”字面量类型相关的边缘情况需要处理。

🌐 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秒。

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

🌐 Reduced Calls into Scanner for JSDoc Parsing

当旧版本的 TypeScript 解析 JSDoc 注释时,它们会使用扫描器/分词器将注释分解为细粒度的标记,然后将内容重新组合起来。这对于规范化注释文本可能有帮助,例如将多个空格合并为一个;但它非常“唠叨”,意味着解析器和扫描器会频繁地来回切换,从而增加了 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 的快照解析时间减少了大约 300ms,使其加载和分析速度更快。

打破变更

🌐 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.

在此查看有关此更改的更多信息

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

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

🌐 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.