仅类型导入和导出
🌐 Type-Only Imports and Export
这个功能大多数用户可能永远不需要考虑;然而,如果你在 isolatedModules、TypeScript 的 transpileModule API 或 Babel 下遇到问题,这个功能可能就相关了。
🌐 This feature is something most users may never have to think about; however, if you’ve hit issues under isolatedModules, TypeScript’s transpileModule API, or Babel, this feature might be relevant.
TypeScript 3.8 为仅类型导入和导出添加了新的语法。
🌐 TypeScript 3.8 adds a new syntax for type-only imports and exports.
tsimport type { SomeThing } from "./some-module.js";export type { SomeThing };
import type 仅导入用于类型注解和声明的声明。它 总是 会被完全删除,因此在运行时不会留下任何痕迹。类似地,export type 仅提供可以在类型上下文中使用的导出,并且也会从 TypeScript 的输出中被删除。
需要注意的是,类在运行时有一个值,在设计时有一个类型,并且其使用是上下文敏感的。当使用 import type 导入一个类时,你不能像继承它那样使用它。
🌐 It’s important to note that classes have a value at runtime and a type at design-time, and the use is context-sensitive.
When using import type to import a class, you can’t do things like extend from it.
tsimport type { Component } from "react";interface ButtonProps {// ...}class Button extends Component<ButtonProps> {// ~~~~~~~~~// error! 'Component' only refers to a type, but is being used as a value here.// ...}
如果你以前使用过 Flow,语法大体上是相似的。一个不同之处是,我们添加了一些限制,以避免代码看起来含糊不清。
🌐 If you’ve used Flow before, the syntax is fairly similar. One difference is that we’ve added a few restrictions to avoid code that might appear ambiguous.
ts// Is only 'Foo' a type? Or every declaration in the import?// We just give an error because it's not clear.import type Foo, { Bar, Baz } from "some-module";// ~~~~~~~~~~~~~~~~~~~~~~// error! A type-only import can specify a default import or named bindings, but not both.
与 import type 一起,TypeScript 3.8 还新增了一个编译器标志,用于控制在运行时不会被使用的导入的处理方式:importsNotUsedAsValues。
该标志有三种不同的取值:
🌐 In conjunction with import type, TypeScript 3.8 also adds a new compiler flag to control what happens with imports that won’t be utilized at runtime: importsNotUsedAsValues.
This flag takes 3 different values:
remove:这是今天关于去掉这些导入的行为。它将继续作为默认设置,并且是非破坏性的更改。preserve:这会保留所有值从未被使用的导入。这可能导致导入或副作用被保留。error:这会保留所有导入(与preserve选项相同),但当某个值导入仅用作类型时会报错。如果你想确保没有值被意外导入,但仍然明确副作用导入,这可能会很有用。
有关该功能的更多信息,你可以查看拉取请求,以及相关更改,这些更改涉及扩展 import type 声明的导入可使用范围。
🌐 For more information about the feature, you can take a look at the pull request, and relevant changes around broadening where imports from an import type declaration can be used.
ECMAScript 私有字段
🌐 ECMAScript Private Fields
TypeScript 3.8 支持 ECMAScript 的私有字段,这是 stage-3 类字段提案 的一部分。
🌐 TypeScript 3.8 brings support for ECMAScript’s private fields, part of the stage-3 class fields proposal.
tsclass 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;// ~~~~~// Property '#name' is not accessible outside class 'Person'// because it has a private identifier.
与普通属性(即使是使用 private 修饰符声明的属性)不同,私有字段有一些需要注意的规则。它们包括:
🌐 Unlike regular properties (even ones declared with the private modifier), private fields have a few rules to keep in mind.
Some of them are:
- 私有字段以
#字符开头。有时我们称这些为_私有名_。 - 每个私有字段名称的作用域都唯一限定于其包含类。
- TypeScript 的可访问性修饰符,如
public或private,不能用于私有字段。 - 私有字段不能被类外访问,甚至无法被检测到——即使是 JavaScript 用户也不行!有时我们称之为“严格私有”。
除了“严格”的隐私之外,私有字段的另一个好处是我们刚才提到的唯一性。例如,普通的属性声明很容易在子类中被覆盖。
🌐 Apart from “hard” privacy, another benefit of private fields is that uniqueness we just mentioned. For example, regular property declarations are prone to being overwritten in subclasses.
tsclass C {foo = 10;cHelper() {return this.foo;}}class D extends C {foo = 20;dHelper() {return this.foo;}}let instance = new D();// 'this.foo' refers to the same property on each instance.console.log(instance.cHelper()); // prints '20'console.log(instance.dHelper()); // prints '20'
使用私有字段,你永远不必担心这个问题,因为每个字段名称对于包含的类都是唯一的。
🌐 With private fields, you’ll never have to worry about this, since each field name is unique to the containing class.
tsclass C {#foo = 10;cHelper() {return this.#foo;}}class D extends C {#foo = 20;dHelper() {return this.#foo;}}let instance = new D();// 'this.#foo' refers to a different field within each class.console.log(instance.cHelper()); // prints '10'console.log(instance.dHelper()); // prints '20'
另一件值得注意的事情是,访问任何其他类型上的私有字段都会导致 TypeError!
🌐 Another thing worth noting is that accessing a private field on any other type will result in a TypeError!
tsclass 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 };// Boom!// TypeError: attempted to get private field on non-instance// This fails because 'b' is not an instance of 'Square'.console.log(a.equals(b));
最后,对于任何普通的 .js 文件用户,私有字段必须在赋值之前声明。
🌐 Finally, for any plain .js file users, private fields always have to be declared before they’re assigned to.
jsclass C {// No declaration for '#foo'// :(constructor(foo: number) {// SyntaxError!// '#foo' needs to be declared before writing to it.this.#foo = foo;}}
JavaScript 一直允许用户访问未声明的属性,而 TypeScript 一直要求对类属性进行声明。无论我们是在 .js 还是 .ts 文件中工作,使用私有字段时始终需要声明。
🌐 JavaScript has always allowed users to access undeclared properties, whereas TypeScript has always required declarations for class properties.
With private fields, declarations are always needed regardless of whether we’re working in .js or .ts files.
jsclass C {/** @type {number} */#foo;constructor(foo: number) {// This works.this.#foo = foo;}}
有关实现的更多信息,你可以查看原始拉取请求
🌐 For more information about the implementation, you can check out the original pull request
我应该使用哪种类型?
🌐 Which should I use?
我们已经收到了许多关于作为 TypeScript 用户应该使用哪种私有属性的问题:最常见的是,“我应该使用 private 关键字,还是 ECMAScript 的哈希/井号 (#) 私有字段?”这取决于情况!
🌐 We’ve already received many questions on which type of privates you should use as a TypeScript user: most commonly, “should I use the private keyword, or ECMAScript’s hash/pound (#) private fields?”
It depends!
说到属性,TypeScript 的 private 修饰符会被完全擦除——这意味着在运行时,它的行为完全像普通属性,并且无法区分它是否使用了 private 修饰符声明。使用 private 关键字时,私有性仅在编译时/设计时强制执行,对于 JavaScript 使用者而言,完全是基于意图的。
🌐 When it comes to properties, TypeScript’s private modifiers are fully erased - that means that at runtime, it acts entirely like a normal property and there’s no way to tell that it was declared with a private modifier. When using the private keyword, privacy is only enforced at compile-time/design-time, and for JavaScript consumers it’s entirely intent-based.
tsclass C {private foo = 10;}// This is an error at compile time,// but when TypeScript outputs .js files,// it'll run fine and print '10'.console.log(new C().foo); // prints '10'// ~~~// error! Property 'foo' is private and only accessible within class 'C'.// TypeScript allows this at compile-time// as a "work-around" to avoid the error.console.log(new C()["foo"]); // prints '10'
好处是,这种“软隐私”可以帮助你的消费者暂时绕过无法访问某些 API 的问题,并且在任何运行时环境中都能使用。
🌐 The upside is that this sort of “soft privacy” can help your consumers temporarily work around not having access to some API, and also works in any runtime.
另一方面,ECMAScript 的 # 私有属性在类外是完全无法访问的。
🌐 On the other hand, ECMAScript’s # privates are completely inaccessible outside of the class.
tsclass C {#foo = 10;}console.log(new C().#foo); // SyntaxError// ~~~~// TypeScript reports an error *and*// this won't work at runtime!console.log(new C()["#foo"]); // prints undefined// ~~~~~~~~~~~~~~~// TypeScript reports an error under 'noImplicitAny',// and this prints 'undefined'.
这种严格的私有性对于严格确保没有人可以利用你的任何内部内容非常有用。如果你是库的作者,删除或重命名私有字段绝不应该导致破坏性更改。
🌐 This hard privacy is really useful for strictly ensuring that nobody can take use of any of your internals. If you’re a library author, removing or renaming a private field should never cause a breaking change.
正如我们提到的,另一个好处是使用 ECMAScript 的 # 私有成员进行子类化会更容易,因为它们真的是私有的。使用 ECMAScript # 私有字段时,子类根本不需要担心字段命名冲突。至于 TypeScript 的 private 属性声明,用户仍然需要小心不要覆盖超类中声明的属性。
🌐 As we mentioned, another benefit is that subclassing can be easier with ECMAScript’s # privates because they really are private.
When using ECMAScript # private fields, no subclass ever has to worry about collisions in field naming.
When it comes to TypeScript’s private property declarations, users still have to be careful not to trample over properties declared in superclasses.
还有一件需要考虑的事情是你打算让代码运行在哪个环境中。TypeScript 当前无法支持此功能,除非目标是 ECMAScript 2015(ES6)或更高版本。这是因为我们的降级实现使用 WeakMap 来强制实现私有性,而 WeakMap 无法通过不引起内存泄漏的方式进行 polyfill。相比之下,TypeScript 使用 private 声明的属性可以在所有目标环境中工作——甚至 ECMAScript 3!
🌐 One more thing to think about is where you intend for your code to run.
TypeScript currently can’t support this feature unless targeting ECMAScript 2015 (ES6) targets or higher.
This is because our downleveled implementation uses WeakMaps to enforce privacy, and WeakMaps can’t be polyfilled in a way that doesn’t cause memory leaks.
In contrast, TypeScript’s private-declared properties work with all targets - even ECMAScript 3!
最后一个考虑因素可能是速度:private 属性与其他属性没有区别,因此无论目标运行时是什么,访问它们的速度与访问其他属性一样快。相比之下,由于 # 私有字段是通过 WeakMap 降级的,它们的使用可能会更慢。虽然一些运行时可能会优化它们对 # 私有字段的实际实现,甚至拥有快速的 WeakMap 实现,但在所有运行时中情况可能并非如此。
🌐 A final consideration might be speed: private properties are no different from any other property, so accessing them is as fast as any other property access no matter which runtime you target.
In contrast, because # private fields are downleveled using WeakMaps, they may be slower to use.
While some runtimes might optimize their actual implementations of # private fields, and even have speedy WeakMap implementations, that might not be the case in all runtimes.
export * as ns 语法
🌐 export * as ns Syntax
通常有一个入口点将另一个模块的所有成员作为单个成员公开。
🌐 It’s often common to have a single entry-point that exposes all the members of another module as a single member.
tsimport * as utilities from "./utilities.js";export { utilities };
这非常常见,以至于 ECMAScript 2020 最近添加了新的语法来支持这种模式!
🌐 This is so common that ECMAScript 2020 recently added a new syntax to support this pattern!
tsexport * as utilities from "./utilities.js";
这是对 JavaScript 的一个很好的生活质量改进,TypeScript 3.8 实现了这种语法。当你的模块目标早于 es2020 时,TypeScript 将输出类似于第一个代码片段的内容。
🌐 This is a nice quality-of-life improvement to JavaScript, and TypeScript 3.8 implements this syntax.
When your module target is earlier than es2020, TypeScript will output something along the lines of the first code snippet.
顶层 await
🌐 Top-Level await
TypeScript 3.8 提供了对一个即将到来的 ECMAScript 特性“顶层 await”的支持。
🌐 TypeScript 3.8 provides support for a handy upcoming ECMAScript feature called “top-level await“.
JavaScript 用户经常引入一个 async 函数以使用 await,然后在定义后立即调用该函数。
🌐 JavaScript users often introduce an async function in order to use await, and then immediately called the function after defining it.
jsasync 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。
🌐 This is because previously in JavaScript (along with most other languages with a similar feature), await was only allowed within the body of an async function.
However, with top-level await, we can use await at the top level of a module.
tsconst response = await fetch("...");const greeting = await response.text();console.log(greeting);// Make sure we're a moduleexport {};
请注意有一个微妙之处:顶层 await 仅在 模块 的顶层有效,而文件只有在 TypeScript 发现 import 或 export 时才被视为模块。在某些基本情况下,你可能需要写出 export {} 作为一些模板代码以确保这一点。
🌐 Note there’s a subtlety: top-level await only works at the top level of a module, and files are only considered modules when TypeScript finds an import or an export.
In some basic cases, you might need to write out export {} as some boilerplate to make sure of this.
在你可能预期的所有环境中,顶层 await 可能无法工作。目前,只有在 target 编译器选项为 es2017 或更高版本,并且 module 为 esnext 或 system 时,你才能使用顶层 await。在某些环境和打包工具中的支持可能有限,或者可能需要启用实验性支持。
🌐 Top level await may not work in all environments where you might expect at this point.
Currently, you can only use top level await when the target compiler option is es2017 or above, and module is esnext or system.
Support within several environments and bundlers may be limited or may require enabling experimental support.
有关我们实现的更多信息,你可以查看原始拉取请求。
🌐 For more information on our implementation, you can check out the original pull request.
es2020 适用于 target 和 module
🌐 es2020 for target and module
TypeScript 3.8 支持将 es2020 作为 module 和 target 的选项。
这将保留较新的 ECMAScript 2020 特性,如可选链(optional chaining)、空值合并(nullish coalescing)、export * as ns 和动态 import(...) 语法。
这也意味着 bigint 字面量现在在 esnext 以下拥有稳定的 target。
🌐 TypeScript 3.8 supports es2020 as an option for module and target.
This will preserve newer ECMAScript 2020 features like optional chaining, nullish coalescing, export * as ns, and dynamic import(...) syntax.
It also means bigint literals now have a stable target below esnext.
JSDoc 属性修饰符
🌐 JSDoc Property Modifiers
TypeScript 3.8 通过开启 allowJs 标志支持 JavaScript 文件,并且还可以通过 checkJs 选项或在 .js 文件顶部添加 // @ts-check 注释来对这些 JavaScript 文件进行类型检查。
🌐 TypeScript 3.8 supports JavaScript files by turning on the allowJs flag, and also supports type-checking those JavaScript files via the checkJs option or by adding a // @ts-check comment to the top of your .js files.
由于 JavaScript 文件没有专用的类型检查语法,TypeScript 利用 JSDoc。TypeScript 3.8 理解一些用于属性的新 JSDoc 标签。
🌐 Because JavaScript files don’t have dedicated syntax for type-checking, TypeScript leverages JSDoc. TypeScript 3.8 understands a few new JSDoc tags for properties.
首先是访问修饰符:@public、@private 和 @protected。这些标签的作用与 TypeScript 中的 public、private 和 protected 相对应,功能完全相同。
🌐 First are the accessibility modifiers: @public, @private, and @protected.
These tags work exactly like public, private, and protected respectively work in TypeScript.
js// @ts-checkclass Foo {constructor() {/** @private */this.stuff = 100;}printStuff() {console.log(this.stuff);}}new Foo().stuff;// ~~~~~// error! Property 'stuff' is private and only accessible within class 'Foo'.
@public总是默认存在,可以省略,但它表示某个属性可以从任意位置访问。@private意味着一个属性只能在其所在的类中使用。@protected意味着一个属性只能在包含它的类及其所有派生子类中使用,而不能在包含类的不同实例上使用。
接下来,我们还添加了 @readonly 修饰符,以确保某个属性仅在初始化期间被写入。
🌐 Next, we’ve also added the @readonly modifier to ensure that a property is only ever written to during initialization.
js// @ts-checkclass Foo {constructor() {/** @readonly */this.stuff = 100;}writeToStuff() {this.stuff = 200;// ~~~~~// Cannot assign to 'stuff' because it is a read-only property.}}new Foo().stuff++;// ~~~~~// Cannot assign to 'stuff' because it is a read-only property.
在 Linux 和 watchOptions 上更好地监控目录
🌐 Better Directory Watching on Linux and watchOptions
TypeScript 3.8 推出了一种新的目录监视策略,这对于高效捕捉 node_modules 的变化至关重要。
🌐 TypeScript 3.8 ships a new strategy for watching directories, which is crucial for efficiently picking up changes to node_modules.
作为一些背景,在像 Linux 这样的操作系统上,TypeScript 会在 node_modules 及其许多子目录上安装目录监视器(而不是文件监视器)来检测依赖的变化。这是因为可用的文件监视器数量通常会被 node_modules 中的文件数量所掩盖,而需要跟踪的目录数量要少得多。
🌐 For some context, on operating systems like Linux, TypeScript installs directory watchers (as opposed to file watchers) on node_modules and many of its subdirectories to detect changes in dependencies.
This is because the number of available file watchers is often eclipsed by the number of files in node_modules, whereas there are way fewer directories to track.
较旧版本的 TypeScript 会立即在文件夹上安装目录监听器,在启动时这没有问题;然而,在执行 npm install 时,node_modules 目录内会有大量活动,这可能会让 TypeScript 不堪重负,通常会导致编辑器运行速度大幅下降。
为防止这种情况,TypeScript 3.8 会在安装目录监听器前稍作等待,以便这些高度易变的目录有时间稳定下来。
🌐 Older versions of TypeScript would immediately install directory watchers on folders, and at startup that would be fine; however, during an npm install, a lot of activity will take place within node_modules and that can overwhelm TypeScript, often slowing editor sessions to a crawl.
To prevent this, TypeScript 3.8 waits slightly before installing directory watchers to give these highly volatile directories some time to stabilize.
由于每个项目在不同策略下可能表现更好,而这种新方法可能不适合你的工作流程,TypeScript 3.8 在 tsconfig.json 和 jsconfig.json 中引入了一个新的 watchOptions 字段,允许用户告诉编译器/语言服务应使用哪些监视策略来跟踪文件和目录。
🌐 Because every project might work better under different strategies, and this new approach might not work well for your workflows, TypeScript 3.8 introduces a new watchOptions field in tsconfig.json and jsconfig.json which allows users to tell the compiler/language service which watching strategies should be used to keep track of files and directories.
{// Some typical compiler options"": {"": "es2020","": "node"// ...},// NEW: Options for file/directory watching"watchOptions": {// Use native file system events for files and directories"": "useFsEvents","": "useFsEvents",// Poll files for updates more frequently// when they're updated a lot."": "dynamicPriority"}}
watchOptions 包含 4 个可以配置的新选项:
watchFile:用于设置单个文件如何被监视的策略。可以设置为fixedPollingInterval:以固定间隔每秒检查每个文件是否有更改。priorityPollingInterval:每秒检查每个文件是否有更改,但使用启发式方法检查某些类型的文件的频率要低于其他文件。dynamicPriorityPolling:使用动态队列,其中修改频率较低的文件将较少被检查。useFsEvents(默认):尝试使用操作系统/文件系统的本地事件来监控文件变化。useFsEventsOnParentDirectory:尝试使用操作系统/文件系统的本地事件来监听文件所在目录的更改。这可以减少文件监视器的使用,但可能不够准确。
watchDirectory:在缺乏递归文件监视功能的系统中,用于监视整个目录树的策略。可以设置为:fixedPollingInterval:以固定间隔每秒检查每个目录是否有更改。dynamicPriorityPolling:使用动态队列,其中修改频率较低的目录将较少被检查。useFsEvents(默认):尝试使用操作系统/文件系统的本地事件来监视目录变化。
fallbackPolling:当使用文件系统事件时,此选项指定当系统耗尽本地文件监视器和/或不支持本地文件监视器时使用的轮询策略。可以设置为fixedPollingInterval:(见上文。)priorityPollingInterval:(见上文。)dynamicPriorityPolling:(见上文。)synchronousWatchDirectory:禁用目录的延迟监控。延迟监控在大量文件可能同时发生变化时很有用(例如,通过运行npm install导致node_modules的变化),但在一些不常见的配置下,你可能希望使用此标志将其禁用。
有关这些更改的更多信息,请前往 GitHub 查看拉取请求 以了解详细内容。
🌐 For more information on these changes, head over to GitHub to see the pull request to read more.
“快而松”的增量检查
🌐 “Fast and Loose” Incremental Checking
TypeScript 3.8 引入了一个名为 assumeChangesOnlyAffectDirectDependencies 的新编译器选项。当启用此选项时,TypeScript 将避免重新检查/重建所有可能受影响的文件,只会重新检查/重建已更改的文件以及直接导入它们的文件。
🌐 TypeScript 3.8 introduces a new compiler option called assumeChangesOnlyAffectDirectDependencies.
When this option is enabled, TypeScript will avoid rechecking/rebuilding all truly possibly-affected files, and only recheck/rebuild files that have changed as well as files that directly import them.
例如,考虑一个文件 fileD.ts,它导入 fileC.ts,而 fileC.ts 导入 fileB.ts,fileB.ts 又导入 fileA.ts,如下所示:
🌐 For example, consider a file fileD.ts that imports fileC.ts that imports fileB.ts that imports fileA.ts as follows:
fileA.ts <- fileB.ts <- fileC.ts <- fileD.ts
在 --watch 模式下,fileA.ts 的变化通常意味着 TypeScript 至少需要重新检查 fileB.ts、fileC.ts 和 fileD.ts。
在 assumeChangesOnlyAffectDirectDependencies 下,fileA.ts 的变化意味着仅需要重新检查 fileA.ts 和 fileB.ts。
🌐 In --watch mode, a change in fileA.ts would typically mean that TypeScript would need to at least re-check fileB.ts, fileC.ts, and fileD.ts.
Under assumeChangesOnlyAffectDirectDependencies, a change in fileA.ts means that only fileA.ts and fileB.ts need to be re-checked.
在像 Visual Studio Code 这样的代码库中,这将某些文件更改的重建时间从大约 14 秒缩短到大约 1 秒。
虽然我们不一定推荐在所有代码库中使用此选项,但如果你有一个非常大的代码库,并且愿意将完整的项目错误推迟到稍后(例如,通过 tsconfig.fullbuild.json 进行的专用构建或在持续集成中),你可能会对此感兴趣。
🌐 In a codebase like Visual Studio Code, this reduced rebuild times for changes in certain files from about 14 seconds to about 1 second.
While we don’t necessarily recommend this option for all codebases, you might be interested if you have an extremely large codebase and are willing to defer full project errors until later (e.g. a dedicated build via a tsconfig.fullbuild.json or in CI).
欲了解更多详情,你可以查看原始拉取请求。
🌐 For more details, you can see the original pull request.