Skip to content
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待

TypeScript 5.1

简化返回 undefined 函数的隐式返回

在 JavaScript 中,如果一个函数运行结束而没有遇到 return,它会返回值 undefined

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

然而,在以前版本的 TypeScript 中,唯一可以完全没有 return 语句的函数是返回 voidany 的函数。 这意味着即使你明确说“这个函数返回 undefined”,你也被迫至少有一个 return 语句。

ts
// ✅ 没问题 - 我们推断 'f1' 返回 'void'
function f1() {
    // 没有 return
}
// ✅ 没问题 - 'void' 不需要 return 语句
function f2(): void {
    // 没有 return
}
// ✅ 没问题 - 'any' 不需要 return 语句
function f3(): any {
    // 没有 return
}
// ❌ 错误!
// 声明类型既不是 'void' 也不是 'any' 的函数必须返回一个值。
function f4(): undefined {
    // 没有 return
}

如果某个 API 期望一个返回 undefined 的函数,这可能会很痛苦——你需要至少有一个显式的 undefined 返回,或者一个 return 语句一个显式的注解。

ts
declare function takesFunction(f: () => undefined): undefined;
// ❌ 错误!
// 类型 '() => void' 的参数不能赋给类型 '() => undefined' 的参数。
takesFunction(() => {
    // 没有 return
});
// ❌ 错误!
// 声明类型既不是 'void' 也不是 'any' 的函数必须返回一个值。
takesFunction((): undefined => {
    // 没有 return
});
// ❌ 错误!
// 类型 '() => void' 的参数不能赋给类型 '() => undefined' 的参数。
takesFunction(() => {
    return;
});
// ✅ 有效
takesFunction(() => {
    return undefined;
});
// ✅ 有效
takesFunction((): undefined => {
    return;
});

这种行为令人沮丧和困惑,尤其是在调用自己无法控制的函数时。 理解推断 voidundefined 之间的相互作用、返回 undefined 的函数是否需要 return 语句等似乎是一种干扰。

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

ts
// ✅ 在 TypeScript 5.1 中有效!
function f4(): undefined {
    // 没有 return
}
// ✅ 在 TypeScript 5.1 中有效!
takesFunction((): undefined => {
    // 没有 return
});

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

ts
// ✅ 在 TypeScript 5.1 中有效!
takesFunction(function f() {
    //                 ^ 返回类型是 undefined
    // 没有 return
});
// ✅ 在 TypeScript 5.1 中有效!
takesFunction(function f() {
    //                 ^ 返回类型是 undefined
    return;
});

为了解决另一个类似的痛点,在 TypeScript 的 --noImplicitReturns 选项下,仅返回 undefined 的函数现在具有与 void 类似的例外,即并非每个代码路径都必须以显式的 return 结束。

ts
// ✅ 在 TypeScript 5.1 的 '--noImplicitReturns' 下有效!
function f(): undefined {
    if (Math.random()) {
        // 做一些事情...
        return;
    }
}

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

getter 和 setter 的不相关类型

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

ts
interface Serializer {
    set value(v: string | number | boolean);
    get value(): string;
}
declare let box: Serializer;
// 允许写入一个 'boolean'
box.value = true;
// 输出为 'string'
console.log(box.value.toUpperCase());

最初我们要求 get 类型必须是 set 类型的子类型。 这意味着编写

ts
box.value = box.value;

总是有效的。

然而,存在大量现有的和提议的 API,其 getter 和 setter 之间具有完全不相关的类型。 例如,考虑最常见的例子之一——DOM 中的 style 属性和 CSSStyleRule API。 每个样式规则都有一个 style 属性,它是一个 CSSStyleDeclaration; 但是,如果你尝试写入该属性,它只有用字符串才能正确工作!

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

ts
interface CSSStyleRule {
    // ...
    /** 总是读取为 `CSSStyleDeclaration` */
    get style(): CSSStyleDeclaration;
    /** 这里只能写入 `string`。 */
    set style(newValue: string);
    // ...
}

这也允许其他模式,比如要求 set 访问器只接受“有效”数据,但指定如果某些底层状态尚未初始化,get 访问器可能返回 undefined

ts
class SafeBox {
    #value: string | undefined;
    // 只接受字符串!
    set value(newValue: string) {
    }
    // 必须检查 'undefined'!
    get value(): string | undefined {
        return this.#value;
    }
}

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

你可以在实现拉取请求中阅读更多内容。

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

TypeScript 在 JSX 方面的一个痛点是它对每个 JSX 元素标签类型的要求。

具体来说,JSX 元素是以下之一:

tsx
// 自闭合 JSX 标签
<Foo />
// 带有开始/结束标签的常规元素
<Bar></Bar>

当类型检查 <Foo /><Bar></Bar> 时,TypeScript 总是查找一个名为 JSX 的命名空间,并从中获取一个名为 Element 的类型——或者更直接地说,它查找 JSX.Element

但是要检查 FooBar 本身是否可以用作标签名,TypeScript 大致只是获取 FooBar 返回或构造的类型,并检查与 JSX.Element(或者如果类型是可构造的,则检查另一个名为 JSX.ElementClass 的类型)的兼容性。

这里的限制意味着,如果组件返回或“渲染”的类型比 JSX.Element 更宽泛,则无法使用它们。 例如,一个 JSX 库可能允许组件返回 stringPromise

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

tsx
import * as React from "react";
async function Foo() {
    return <div></div>;
}
let element = <Foo />;
//             ~~~
// 'Foo' 不能用作 JSX 组件。
//   它的返回类型 'Promise<Element>' 不是有效的 JSX 元素。

为了给库提供表达这一点的方法,TypeScript 5.1 现在查找一个名为 JSX.ElementType 的类型。 ElementType 精确指定了哪些可以用作 JSX 元素中的标签。 所以它今天可能被类型化为类似这样的东西:

tsx
namespace JSX {
    export type ElementType =
        // 所有有效的小写标签
        keyof IntrinsicAttributes
        // 函数组件
        (props: any) => Element
        // 类组件
        new (props: any) => ElementClass;
    export interface IntrinsicAttributes extends /*...*/ {}
    export type Element = /*...*/;
    export type ElementClass = /*...*/;
}

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

命名空间的 JSX 属性

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

tsx
import * as React from "react";
// 这两个是等价的:
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 上查找。

tsx
// 在某个库的代码或该库的扩展中:
namespace JSX {
    interface IntrinsicElements {
        ["a:b"]: { prop: string };
    }
}
// 在我们的代码中:
let x = <a:b prop="hello!" />;

此贡献Oleksandr Tarasiuk 提供。

在模块解析中查阅 typeRoots

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

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

将声明移动到现有文件

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

将函数 'getThanks' 移动到工作区中的现有文件。

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

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

JSX 标签的链接光标

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

一个 JSX 标签示例,链接编辑修改了一个 JSX 片段和一个 div 元素。

这个新功能应该在 TypeScript 和 JavaScript 文件中都能工作,并且可以在 Visual Studio Code Insiders 中启用。 在 Visual Studio Code 中,你可以在设置 UI 中编辑 Editor: Linked Editing 选项:

Visual Studio Code 的 Editor: Linked Editing 选项

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

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

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

你可以在此处查看我们的链接编辑实现

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

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

一个在 'add' 函数上补全 JSDoc param 注释的示例。

你可以在 GitHub 上查看这个新功能是如何实现的

优化

避免不必要的类型实例化

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

你可以在 GitHub 上查看与此更改相关的更改

联合字面量的否定情况检查

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

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

此优化能够将此问题中的代码的类型检查时间从大约 45 秒减少到大约 0.4 秒。

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

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

TypeScript 5.1 将更多关于将 JSDoc 注释分解的逻辑移入了扫描器/分词器。 扫描器现在将更大的内容块直接返回给解析器,以供其按需处理。

这些更改将几个 10MB 大小的主要包含散文注释的 JavaScript 文件的解析时间减少了一半。 对于更现实的例子,我们的性能套件中 xstate 的快照解析时间减少了约 300 毫秒,使其加载和分析更快。

破坏性更改

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.jstsserver.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 收到如下错误消息:

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:

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 的向上遍历

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

这种行为可能导致过多的查找,在 TypeScript 5.1 中已被禁用。 因此,你可能会根据 tsconfig.jsontypes 选项或 /// <reference > 指令中的条目开始看到如下错误:

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

jsonc
{
    "compilerOptions": {
        "types": [
            "node",
            "mocha"
        ],
        "typeRoots": [
            // 保留你之前已有的内容。
            "./some-custom-types/",
            // 你可能需要本地的 'node_modules/@types'。
            "./node_modules/@types",
            // 如果你使用 "monorepo" 布局,可能还需要指定共享的 'node_modules/@types'。
            "../../node_modules/@types",
        ]
    }
}

更多信息可在我们的问题跟踪器上的原始更改中找到。