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

TypeScript 3.8

仅类型导入和导出

此功能是大多数用户可能永远不需要考虑的;但是,如果你在 isolatedModules、TypeScript 的 transpileModule API 或 Babel 下遇到问题,此功能可能与之相关。

TypeScript 3.8 为仅类型导入和导出添加了新语法。

ts
import type { SomeThing } from "./some-module.js";

export type { SomeThing };

import type 仅导入用于类型注解和声明的声明。 它总是被完全擦除,因此在运行时没有它的残留。 类似地,export type 仅提供可用于类型上下文的导出,并且也会从 TypeScript 的输出中擦除。

需要注意的是,类在运行时具有值,在设计时具有类型,并且使用是上下文相关的。 当使用 import type 导入一个类时,你不能做像扩展它这样的事情。

ts
import type { Component } from "react";

interface ButtonProps {
  // ...
}

class Button extends Component<ButtonProps> {
  //               ~~~~~~~~~
  // 错误!'Component' 仅指类型,但在此处被用作值。
  // ...
}

如果你以前使用过 Flow,语法非常相似。 一个区别是我们添加了一些限制,以避免可能看起来模棱两可的代码。

ts
// 只有 'Foo' 是类型?还是导入中的每个声明?
// 我们只是给出一个错误,因为不清楚。

import type Foo, { Bar, Baz } from "some-module";
//     ~~~~~~~~~~~~~~~~~~~~~~
// 错误!仅类型导入可以指定默认导入或命名绑定,但不能同时指定两者。

import type 一起,TypeScript 3.8 还添加了一个新的编译器标志来控制运行时不会使用的导入会发生什么: importsNotUsedAsValues。 此标志采用 3 个不同的值:

  • remove:这是今天删除这些导入的行为。它将继续是默认值,并且是一个非破坏性更改。
  • preserve:这保留所有从未使用过其值的导入。这可能导致导入/副作用被保留。
  • error:这保留所有导入(与 preserve 选项相同),但当值导入仅用作类型时会报错。如果你想确保没有意外导入值,但仍使副作用导入显式化,这可能很有用。

有关此功能的更多信息,你可以查看拉取请求,以及关于拓宽 import type 声明中导入可用位置的相关更改

ECMAScript 私有字段

TypeScript 3.8 支持 ECMAScript 的私有字段,这是第 3 阶段类字段提案的一部分。

ts
class Person {
  #name: string;

  constructor(name: string) {
    this.#name = name;
  }

  greet() {
    console.log(`Hello, my name is ${this.#name}!`);
  }
}

let jeremy = new Person("Jeremy Bearimy");

jeremy.#name;
//     ~~~~~
// 属性 '#name' 在类 'Person' 外部不可访问,因为它具有私有标识符。

与常规属性(即使是使用 private 修饰符声明的属性)不同,私有字段有一些规则需要记住。 其中一些是:

  • 私有字段以 # 字符开头。有时我们称这些为私有名称
  • 每个私有字段名称在其包含的类中是唯一作用域的。
  • TypeScript 的可访问性修饰符(如 publicprivate)不能用于私有字段。
  • 私有字段在包含类外部无法访问甚至无法检测到——即使对于 JS 用户也是如此!有时我们称此为硬隐私

除了“硬”隐私之外,私有字段的另一个好处是我们刚刚提到的唯一性。 例如,常规属性声明容易在子类中被覆盖。

ts
class C {
  foo = 10;

  cHelper() {
    return this.foo;
  }
}

class D extends C {
  foo = 20;

  dHelper() {
    return this.foo;
  }
}

let instance = new D();
// 'this.foo' 引用每个实例上的同一属性。
console.log(instance.cHelper()); // 打印 '20'
console.log(instance.dHelper()); // 打印 '20'

使用私有字段,你永远不必担心这一点,因为每个字段名称对于包含它的类是唯一的。

ts
class C {
  #foo = 10;

  cHelper() {
    return this.#foo;
  }
}

class D extends C {
  #foo = 20;

  dHelper() {
    return this.#foo;
  }
}

let instance = new D();
// 'this.#foo' 在每个类中引用不同的字段。
console.log(instance.cHelper()); // 打印 '10'
console.log(instance.dHelper()); // 打印 '20'

另一件值得注意的事情是,在任何其他类型上访问私有字段都会导致 TypeError

ts
class Square {
  #sideLength: number;

  constructor(sideLength: number) {
    this.#sideLength = sideLength;
  }

  equals(other: any) {
    return this.#sideLength === other.#sideLength;
  }
}

const a = new Square(100);
const b = { sideLength: 100 };

// 轰!
// TypeError: attempted to get private field on non-instance
// 这失败是因为 'b' 不是 'Square' 的实例。
console.log(a.equals(b));

最后,对于任何纯 .js 文件用户,私有字段始终必须在赋值之前声明。

js
class C {
  // 没有 '#foo' 的声明
  // :(

  constructor(foo: number) {
    // SyntaxError!
    // 在写入 '#foo' 之前需要声明它。
    this.#foo = foo;
  }
}

JavaScript 一直允许用户访问未声明的属性,而 TypeScript 始终要求类属性声明。 对于私有字段,无论我们是在 .js 还是 .ts 文件中工作,总是需要声明。

js
class C {
  /** @type {number} */
  #foo;

  constructor(foo: number) {
    // 这有效。
    this.#foo = foo;
  }
}

有关实现的更多信息,你可以查看原始拉取请求

我应该使用哪一个?

我们已经收到了许多关于作为 TypeScript 用户应该使用哪种私有类型的问题:最常见的是,“我应该使用 private 关键字,还是 ECMAScript 的哈希/井号(#)私有字段?” 这取决于!

当涉及到属性时,TypeScript 的 private 修饰符被完全擦除——这意味着在运行时,它完全像一个普通属性一样工作,并且无法判断它是用 private 修饰符声明的。当使用 private 关键字时,隐私仅在编译时/设计时强制执行,对于 JavaScript 消费者来说,它完全是基于意图的。

ts
class C {
  private foo = 10;
}

// 这在编译时是错误,
// 但当 TypeScript 输出 .js 文件时,
// 它会正常运行并打印 '10'。
console.log(new C().foo); // 打印 '10'
//                  ~~~
// 错误!属性 'foo' 是私有的,只能在类 'C' 内部访问。

// TypeScript 在编译时允许这样做
// 作为避免错误的“解决方法”。
console.log(new C()["foo"]); // 打印 '10'

好处是这种“软隐私”可以帮助你的消费者临时解决无法访问某些 API 的问题,并且在任何运行时都可以工作。

另一方面,ECMAScript 的 # 私有字段在类外部完全无法访问。

ts
class C {
  #foo = 10;
}

console.log(new C().#foo); // SyntaxError
//                  ~~~~
// TypeScript 报告一个错误 *并且*
// 这在运行时不起作用!

console.log(new C()["#foo"]); // 打印 undefined
//          ~~~~~~~~~~~~~~~
// TypeScript 在 'noImplicitAny' 下报告错误,
// 并且这打印 'undefined'。

这种硬隐私对于严格确保没有人可以使用你的任何内部结构非常有用。 如果你是库作者,删除或重命名私有字段不应导致破坏性更改。

正如我们提到的,另一个好处是使用 ECMAScript 的 # 私有字段更容易进行子类化,因为它们真正是私有的。 当使用 ECMAScript # 私有字段时,没有子类需要担心字段命名冲突。 对于 TypeScript 的 private 属性声明,用户仍然需要小心不要覆盖超类中声明的属性。

还有一件事需要考虑的是你打算在哪里运行你的代码。 TypeScript 目前不支持此功能,除非以 ECMAScript 2015(ES6)或更高版本为目标。 这是因为我们的降级实现使用 WeakMap 来强制执行隐私,而 WeakMap 无法在不导致内存泄漏的情况下进行 polyfill。 相比之下,TypeScript 的 private 声明的属性适用于所有目标——甚至是 ECMAScript 3!

最后一个考虑因素可能是速度:private 属性与其他任何属性没有区别,因此无论你针对哪个运行时,访问它们都与其他任何属性访问一样快。 相比之下,因为 # 私有字段是使用 WeakMap 降级的,所以它们使用起来可能会慢一些。 虽然某些运行时可能会优化其 # 私有字段的实际实现,甚至具有快速的 WeakMap 实现,但这可能并非在所有运行时中都是如此。

export * as ns 语法

通常有一个单一的入口点,将另一个模块的所有成员作为一个成员暴露出来是很常见的。

ts
import * as utilities from "./utilities.js";
export { utilities };

这非常常见,以至于 ECMAScript 2020 最近添加了一种新语法来支持这种模式!

ts
export * as utilities from "./utilities.js";

这是对 JavaScript 的一个很好的生活质量改进,TypeScript 3.8 实现了这种语法。 当你的模块目标低于 es2020 时,TypeScript 将输出类似于第一个代码片段的内容。

顶层 await

TypeScript 3.8 提供了一个即将推出的 ECMAScript 功能“顶层 await”的支持。

JavaScript 用户经常引入一个 async 函数来使用 await,然后在定义后立即调用该函数。

js
async function main() {
  const response = await fetch("...");
  const greeting = await response.text();
  console.log(greeting);
}

main().catch((e) => console.error(e));

这是因为以前在 JavaScript 中(以及大多数具有类似功能的其他语言中),await 只允许在 async 函数体内使用。 然而,使用顶层 await,我们可以在模块的顶层使用 await

ts
const response = await fetch("...");
const greeting = await response.text();
console.log(greeting);

// 确保我们是一个模块
export {};

注意有一个微妙之处:顶层 await 仅在模块的顶层工作,并且只有当 TypeScript 找到 importexport 时,文件才被视为模块。 在某些基本情况下,你可能需要写出 export {} 作为一些样板代码来确保这一点。

此时,顶层 await 可能无法在你期望的所有环境中工作。 目前,你只能在 target 编译器选项为 es2017 或更高版本,并且 moduleesnextsystem 时使用顶层 await。 多个环境和打包器中的支持可能有限,或者可能需要启用实验性支持。

有关我们实现的更多信息,你可以查看原始拉取请求

targetmodulees2020

TypeScript 3.8 支持 es2020 作为 moduletarget 的选项。 这将保留较新的 ECMAScript 2020 功能,如可选链、空值合并、export * as ns 和动态 import(...) 语法。 这也意味着 bigint 字面量现在在低于 esnext 的目标下具有稳定的 target

JSDoc 属性修饰符

TypeScript 3.8 通过打开 allowJs 标志来支持 JavaScript 文件,并且还通过 checkJs 选项或在 .js 文件顶部添加 // @ts-check 注释来支持对这些 JavaScript 文件进行类型检查

由于 JavaScript 文件没有用于类型检查的专用语法,TypeScript 利用了 JSDoc。 TypeScript 3.8 理解几个用于属性的新 JSDoc 标签。

首先是可访问性修饰符:@public@private@protected。 这些标签的工作方式分别与 TypeScript 中的 publicprivateprotected 完全相同。

js
// @ts-check

class Foo {
  constructor() {
    /** @private */
    this.stuff = 100;
  }

  printStuff() {
    console.log(this.stuff);
  }
}

new Foo().stuff;
//        ~~~~~
// 错误!属性 'stuff' 是私有的,只能在类 'Foo' 内部访问。
  • @public 总是隐含的,可以省略,但意味着可以从任何地方访问属性。
  • @private 意味着属性只能在包含它的类中使用。
  • @protected 意味着属性只能在包含它的类以及所有派生子类中使用,但不能在包含它的类的不相似实例上使用。

接下来,我们还添加了 @readonly 修饰符,以确保属性仅在初始化期间被写入。

js
// @ts-check

class Foo {
  constructor() {
    /** @readonly */
    this.stuff = 100;
  }

  writeToStuff() {
    this.stuff = 200;
    //   ~~~~~
    // 不能赋值给 'stuff',因为它是只读属性。
  }
}

new Foo().stuff++;
//        ~~~~~
// 不能赋值给 'stuff',因为它是只读属性。

在 Linux 上更好的目录监视和 watchOptions

TypeScript 3.8 提供了一种新的目录监视策略,这对于高效地检测 node_modules 的变化至关重要。

背景是,在像 Linux 这样的操作系统上,TypeScript 在 node_modules 及其许多子目录上安装目录监视器(而不是文件监视器)来检测依赖项的变化。 这是因为可用的文件监视器数量通常被 node_modules 中的文件数量所超过,而需要跟踪的目录数量要少得多。

旧版本的 TypeScript 会立即在文件夹上安装目录监视器,这在启动时没问题;然而,在 npm 安装期间,node_modules 内会发生大量活动,这可能会淹没 TypeScript,常常使编辑器会话变得极其缓慢。 为了防止这种情况,TypeScript 3.8 在安装目录监视器之前稍微等待一下,以给这些高度易变的目录一些稳定时间。

由于每个项目可能在不同的策略下工作得更好,并且这种新方法可能不适合你的工作流程,TypeScript 3.8 在 tsconfig.jsonjsconfig.json 中引入了一个新的 watchOptions 字段,允许用户告诉编译器/语言服务应该使用哪种监视策略来跟踪文件和目录。

jsonc
{
  // 一些典型的编译器选项
  "compilerOptions": {
    "target": "es2020",
    "moduleResolution": "node"
    // ...
  },

  // 新:文件/目录监视的选项
  "watchOptions": {
    // 对文件和目录使用原生文件系统事件
    "watchFile": "useFsEvents",
    "watchDirectory": "useFsEvents",

    // 当文件更新频繁时,更频繁地轮询文件
    "fallbackPolling": "dynamicPriority"
  }
}

watchOptions 包含 4 个可以配置的新选项:

  • watchFile:监视单个文件的策略。可以设置为

    • fixedPollingInterval:以固定间隔每秒检查每个文件几次是否有变化。
    • priorityPollingInterval:每秒检查每个文件几次是否有变化,但使用启发式方法比其他类型的文件更少检查某些类型的文件。
    • dynamicPriorityPolling:使用动态队列,其中修改频率较低的文件将被较少地检查。
    • useFsEvents(默认):尝试使用操作系统/文件系统的原生事件来检测文件更改。
    • useFsEventsOnParentDirectory:尝试使用操作系统/文件系统的原生事件来监听文件所在目录的变化。这可以使用更少的文件监视器,但可能不太准确。
  • watchDirectory:在缺乏递归文件监视功能的系统下,监视整个目录树的策略。可以设置为:

    • fixedPollingInterval:以固定间隔每秒检查每个目录几次是否有变化。
    • dynamicPriorityPolling:使用动态队列,其中修改频率较低的目录将被较少地检查。
    • useFsEvents(默认):尝试使用操作系统/文件系统的原生事件来检测目录更改。
  • fallbackPolling:当使用文件系统事件时,此选项指定当系统用完原生文件监视器和/或不支持原生文件监视器时使用的轮询策略。可以设置为

    • fixedPollingInterval:(见上文)
    • priorityPollingInterval:(见上文)
    • dynamicPriorityPolling:(见上文)
    • synchronousWatchDirectory:禁用目录的延迟监视。当可能同时发生大量文件更改时(例如,运行 npm install 导致 node_modules 中的更改),延迟监视很有用,但对于一些不太常见的设置,你可能希望使用此标志禁用它。

有关这些更改的更多信息,请前往 GitHub 查看拉取请求以阅读更多内容。

“快速松散”增量检查

TypeScript 3.8 引入了一个新的编译器选项,称为 assumeChangesOnlyAffectDirectDependencies。 当启用此选项时,TypeScript 将避免重新检查/重新构建所有真正可能受影响的文件,而只重新检查/重新构建已更改的文件以及直接导入它们的文件。

例如,考虑一个文件 fileD.ts 导入 fileC.tsfileC.ts 导入 fileB.tsfileB.ts 导入 fileA.ts,如下所示:

fileA.ts <- fileB.ts <- fileC.ts <- fileD.ts

--watch 模式下,fileA.ts 中的更改通常意味着 TypeScript 至少需要重新检查 fileB.tsfileC.tsfileD.ts。 在 assumeChangesOnlyAffectDirectDependencies 下,fileA.ts 中的更改意味着只需要重新检查 fileA.tsfileB.ts

在像 Visual Studio Code 这样的代码库中,这使某些文件更改的重建时间从大约 14 秒减少到大约 1 秒。 虽然我们不一定会向所有代码库推荐此选项,但如果你有一个非常大的代码库,并且愿意将完整的项目错误推迟到以后(例如,通过 tsconfig.fullbuild.json 或在 CI 中进行专用构建),你可能会感兴趣。

有关更多详细信息,你可以查看原始拉取请求