TypeScript 3.8 新特性全解析:类型导入、私有字段、顶层 await 与增量检查
文档教程【免费下载链接】TypeScriptTypeScript 使用手册中文版翻译。http://www.typescriptlang.org项目地址https://gitcode.com/gh_mirrors/typ/TypeScript点击查看免费下载导读本文基于 TypeScript 使用手册中文版的 3.8 版本发布说明系统梳理该版本带来的 8 项核心能力import type类型导入/导出、ECMAScript 私有字段#私有名、export * as ns语法、顶层await、es2020目标支持、JSDoc 属性修饰符、Linux 目录监听优化与watchOptions以及 Fast and Loose 增量检查选项。阅读完本文你将掌握这些特性的语法、配置方式、适用场景与取舍依据并了解它们与 编译器选项、tsconfig.json 及 配置 Watch 等周边文档的配合使用方式。一、类型导入和导出Type-Only Imports and Exports大多数用户可能永远不需要考虑这个特性但如果你在使用--isolatedModules、TypeScript 的transpileModuleAPI 或 Babel 时遇到过问题那么这个特性就与你高度相关。TypeScript 3.8 为仅用于类型的导入和导出新增了专用语法import type { SomeThing } from ./some-module.js; export type { SomeThing };import type只会导入那些仅用于类型注解和类型声明的声明它总是会被完全擦除erased运行时不会留下任何残留代码同理export type只提供可供类型上下文使用的导出也会从 TypeScript 的输出中被擦除。注意类不能仅以类型方式使用类在运行时拥有值在设计期拥有类型其用法是上下文敏感的。当你用import type导入一个类时不能对它进行extends继承等值层面的操作import type { Component } from react; interface ButtonProps { // ... } class Button extends ComponentButtonProps { // ~~~~~~~~~ // error! Component only refers to a type, but is being used as a value here. // ... }如果你用过 Flow这个语法与之十分相似。不过 TypeScript 额外加了一些限制以避免出现含义模糊的代码// 到底只有 Foo 是类型还是导入中的所有声明都是类型 // 因为含义不明确这里直接给出错误。 import type Foo, { Bar, Baz } from some-module; // ~~~~~~~~~~~~~~~~~~~~~~ // error! A type-only import can specify a default import or named bindings, but not both.新增编译选项importsNotUsedAsValues与import type配套TypeScript 3.8 新增了一个编译器标志用于控制运行时不会被用到的导入如何处理importsNotUsedAsValues。该标志接受 3 种取值取值行为remove即今天默认的行为丢弃这类导入。它将继续作为默认值属于非破坏性变更。preserve保留所有值从未被使用的导入。这可能导致导入/副作用被保留下来。error保留所有导入同preserve但当某个值导入仅被当作类型使用时报错。若你想确保没有值被意外导入、同时又希望副作用导入保持显式这个选项很有用。在仓库的编译器选项中该选项被记录为设置针对于类型导入的代码生成和代码检查行为remove与preserve决定是否对仅用作类型、未产生副作用的模块导入生成相关代码error则强制要求只用作类型的模块导入必须使用import type语句。补充后续版本如 TypeScript 5.0 引入的--verbatimModuleSyntax提供了更一致的行为并计划弃用importsNotUsedAsValues与--preserveValueImports参见 TypeScript 5.0 发布说明但 3.8 中该选项仍是管理类型导入代码生成的主要手段。二、ECMAScript 私有变量ECMAScript Private FieldsTypeScript 3.8 带来了对 ECMAScript 私有字段的支持它是 class fields 提案stage-3 的一部分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; // ~~~~~ // Property #name is not accessible outside class Person // because it has a private identifier.与普通属性即便是用private修饰符声明的属性不同私有字段有一系列必须牢记的规则私有字段以#字符开头有时被称为私有名private names每个私有字段名都唯一限定在其所属类的作用域内TypeScript 的访问性修饰符如public、private不能用于私有字段私有字段无法在包含它的类之外被访问甚至无法被检测到——即使是 JavaScript 用户也不行这就是所谓的硬隐私hard privacy。私有字段的唯一性优势子类不再互相覆盖除了硬隐私之外私有字段的另一大好处正是上文提到的唯一性。普通属性声明在子类中容易被覆盖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而使用私有字段后你完全不用再担心这个问题因为每个字段名都唯一属于其包含类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值得注意的另一点是在任何其他类型上访问私有字段都会导致TypeErrorclass 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 // 这是因为 b 并不是 Square 的实例。 console.log(a.equals(b));普通.js文件中的强制声明要求最后对于普通.js文件用户私有字段必须在使用赋值之前先声明。JavaScript 一向允许访问未声明的属性而 TypeScript 一向要求类属性必须先声明对于私有字段无论是.js还是.ts文件声明都是必需的class C { // 没有为 #foo 声明 // :( constructor(foo: number) { // SyntaxError! // #foo 需要在写入前先声明。 this.#foo foo; } }先声明再赋值即可正常工作class C { /** type {number} */ #foo; constructor(foo: number) { // 这可以正常工作。 this.#foo foo; } }应该用哪种私有机制很多 TypeScript 用户都问过同一个问题应该用private关键字还是 ECMAScript 的#私有字段答案取决于你的需求。TypeScriptprivate是软隐私就属性而言TypeScript 的private修饰符会被完全擦除——也就是说运行时它表现得与普通属性毫无差别无法看出它曾被private修饰过。使用private关键字时隐私只在编译期/设计期被强制执行对 JavaScript 使用者而言纯粹靠约定自觉class C { private foo 10; } // 编译期这是错误 // 但 TypeScript 输出 .js 文件后 // 它可以正常运行并打印 10。 console.log(new C().foo); // 打印 10 // ~~~ // error! Property foo is private and only accessible within class C. // TypeScript 允许这种绕开编译错误的写法。 console.log(new C()[foo]); // 打印 10这种软隐私的好处是它可以帮助使用者临时绕过某些尚未开放的 API并且在任何运行时环境中都能工作。ECMAScript#是硬隐私另一方面ECMAScript 的#私有字段在类之外完全不可访问class C { #foo 10; } console.log(new C().#foo); // SyntaxError // ~~~~ // TypeScript 会报告错误 *并且* // 运行时也无法工作 console.log(new C()[#foo]); // 打印 undefined // ~~~~~~~~~~~~~~~ // TypeScript 在 noImplicitAny 下会报告错误 // 并且这里会打印 undefined。硬隐私对严格确保没人能利用你的内部实现非常有用。如果你是库作者删除或重命名一个私有字段永远不应构成破坏性变更。子类化与命名冲突ECMAScript 的#私有字段让子类化变得更轻松因为它们真的是私有的任何子类都不必担心字段命名冲突。而使用 TypeScript 的private属性声明时使用者仍需小心不要踩踏超类中声明的属性。目标环境target限制另一个需要考虑的问题是代码的运行环境TypeScript 当前只有在target为 ECMAScript 2015ES6或更高时才支持该特性。因为降级实现使用WeakMap来强制隐私而WeakMap无法以不产生内存泄漏的方式 polyfill。相比之下TypeScript 的private属性适用于所有 target——甚至 ECMAScript 3。性能考量private属性与其他属性没有任何区别无论目标运行时是什么访问速度与其他属性访问一样快由于#私有字段降级时使用WeakMap访问它们可能更慢。虽然某些运行时可能会优化#私有字段的真实实现、甚至拥有高速的WeakMap实现但这并非在所有运行时都成立。小结追求编译期约束 跨运行时兼容选private追求运行时硬隔离 库 API 演进安全选#私有字段。三、export * as ns语法常见需求是让一个单一入口点把另一个模块的所有成员作为单个成员暴露出去。过去通常这样写import * as utilities from ./utilities.js; export { utilities };这个模式太常见了以至于 ECMAScript 2020 为此专门新增了语法export * as utilities from ./utilities.js;这是对 JavaScript 的一个很好的体验改进TypeScript 3.8 实现了这一语法。当你的模块目标module早于es2020时TypeScript 会输出类似于上面第一段代码的形式即先import * as再export {}从而在旧目标下依然保持语义一致。四、顶层awaitTop-Level awaitTypeScript 3.8 支持了一个方便的、即将到来的 ECMAScript 特性——顶层await。此前JavaScript 用户为了使用await往往要引入一个async函数并在定义后立刻调用它async function main() { const response await fetch(...); const greeting await response.text(); console.log(greeting); } main().catch(e console.error(e));这是因为在过去await只被允许出现在async函数体内大多数拥有类似特性的语言也是如此。而有了顶层await我们可以在模块的顶层直接使用awaitconst response await fetch(...); const greeting await response.text(); console.log(greeting); // 确保这是一个模块 export {};使用前提与注意事项顶层await只在模块module的顶层生效而 TypeScript 只有在发现import或export时才把文件视为模块。在一些基础场景中你可能需要写上export {}这类样板代码来确保文件被当作模块处理现阶段3.8 发布时并非所有环境都支持顶层await只有当target编译器选项为es2017或更高、且module为esnext或system时才可使用。多个环境和打包器中的支持可能有限或需要开启实验性支持例如 Webpack 的experiments.topLevelAwait该特性的实现细节可参考对应的 pull request#35813。五、target与module的es2020选项TypeScript 3.8 支持将es2020作为module和target的选项。这会在输出中保留更新的 ECMAScript 2020 特性例如可选链optional chaining?.空值合并nullish coalescing??export * as ns语法动态import(...)语法同时这也意味着bigint字面量现在拥有了一个低于esnext的稳定目标。在仓库的编译器选项中--target的合法值包括ES3默认、ES5、ES6/ES2015、ES2016、ES2017、ES2018、ES2019、ES2020或ESNext其中ESNext对应最新的 ES proposed features 列表。es2020作为 3.8 起新增的稳定目标正好处于ES2019与ESNext之间。六、JSDoc 属性修饰词JSDoc Property ModifiersTypeScript 3.8 通过allowJs标志支持 JavaScript 文件并通过checkJs选项或.js文件顶部的// ts-check注释对这些文件进行类型检查。由于 JavaScript 文件没有专门用于类型检查的语法TypeScript 借助 JSDoc 来实现。TypeScript 3.8 理解几个针对属性的新 JSDoc 标签。访问性修饰符public、private、protected这些标签分别与 TypeScript 中的public、private、protected工作方式完全相同// ts-check class 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修饰符下一步计划添加readonly修饰符确保属性只能在初始化时被修改// ts-check class 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 目录监听与watchOptionsTypeScript 3.8 带来了全新的目录监听策略这对高效捕获node_modules的变化至关重要。背景为什么需要监听目录在 Linux 这类操作系统上TypeScript 会在node_modules及其众多子目录上安装目录监听器而非文件监听器来检测依赖变化。原因在于node_modules中的文件数量常常远超可用的文件监听器数量而需要跟踪的目录数量则少得多。旧版本 TypeScript 会立即在文件夹上安装目录监听器启动时这没问题但在npm install期间node_modules内会发生大量活动这容易压垮 TypeScript常常让编辑器会话卡到几乎无法响应。为防止这种情况TypeScript 3.8 会稍微等待一会儿再安装目录监听器让这些高度易变的目录有时间稳定下来。新增watchOptions配置因为每个项目在不同的策略下表现可能不同而且新方法未必适合所有工作流TypeScript 3.8 在tsconfig.json和jsconfig.json中引入了新的watchOptions字段允许用户告诉编译器/语言服务应该用哪些监听策略来跟踪文件和目录{ // 一些典型的编译器选项 compilerOptions: { target: es2020, moduleResolution: node, // ... }, // 新增用于文件/目录监听的选项 watchOptions: { // 对文件和目录使用原生文件系统事件 watchFile: useFsEvents, watchDirectory: useFsEvents, // 当文件更新频繁时更频繁地轮询文件 fallbackPolling: dynamicPriority, }, }watchOptions包含四种新选项选项含义与可选值watchFile监听单个文件的策略fixedPollingInterval固定时间间隔检查文件更改priorityPollingInterval固定时间间隔检查但使用启发式规则对某些类型的文件检查频率低于其他文件dynamicPriorityPolling使用动态队列不经常修改的文件被较少检查useFsEvents默认尝试使用操作系统/文件系统原生事件监听文件更改useFsEventsOnParentDirectory尝试使用原生事件监听文件、目录的更改这样可以使用较少的文件监听程序但准确性可能较低watchDirectory在缺少递归文件监听功能的系统中监听整个目录树所使用的策略fixedPollingInterval固定时间间隔检查目录树更改dynamicPriorityPolling动态队列较少检查不常修改的目录useFsEvents默认尝试使用原生事件监听目录更改fallbackPolling当使用文件系统事件时指定兜底策略fixedPollingInterval、priorityPollingInterval、dynamicPriorityPolling含义同上synchronousWatchDirectory在目录上禁用延迟监听功能。在可能一次发生大量文件如node_modules更改时非常有用但如果需要一些不太常见的设置可以禁用延迟监听环境变量方式的老配置如果你更习惯环境变量方式配置 Watch 一节还记录了TSC_WATCHFILE与TSC_WATCHDIRECTORY的用法TSC_WATCHFILE可选PriorityPollingInterval、DynamicPriorityPolling、UseFsEvents、UseFsEventsWithFallbackDynamicPolling、UseFsEventsOnParentDirectory默认未指定时在TSC_NONPOLLING_WATCHERtrue时监视文件父目录否则用fs.watchFile以 250ms 超时轮询TSC_WATCHDIRECTORY可选RecursiveDirectoryUsingFsWatchFile、RecursiveDirectoryUsingDynamicPriorityPolling默认使用fs.watch监视目录及子目录在原生支持递归监视的 Windows 上该变量会被忽略。其实现背景是fs.watch使用文件系统事件、CPU 占用低但依赖平台且监听器数量有限Linux 尤其明显fs.watchFile使用轮询、最可靠但消耗 CPU 周期。两者各有优劣watchOptions正是为了让用户按项目特征选择最合适的组合。八、 Fast and Loose 增量检查assumeChangesOnlyAffectDirectDependenciesTypeScript 3.8 引入了一个新的编译器选项assumeChangesOnlyAffectDirectDependencies。启用该选项后TypeScript 会避免重新检查/重建所有真正可能受影响的文件而只重新检查/重建已更改的文件以及直接导入它们的文件。例如考虑这样的导入链fileA.ts - fileB.ts - fileC.ts - fileD.ts即fileD.ts导入fileC.tsfileC.ts导入fileB.tsfileB.ts导入fileA.tsfileA.ts - fileB.ts - fileC.ts - fileD.ts在--watch模式下fileA.ts的改动通常意味着 TypeScript 至少需要重新检查fileB.ts、fileC.ts和fileD.ts在assumeChangesOnlyAffectDirectDependencies下fileA.ts的改动意味着只需要重新检查fileA.ts和fileB.ts。在类似 Visual Studio Code 的代码库中该选项将某些文件的重新构建时间从约 14 秒降到约 1 秒。不过官方并不建议在所有代码库中使用该选项如果你拥有极大规模的代码库并且愿意把完整的项目错误推迟到之后例如通过专门的tsconfig.fullbuild.json或 CI 中执行完整构建那么这个选项可能对你很有吸引力——即以牺牲一部分即时错误反馈为代价换取极快的增量检查速度。总结TypeScript 3.8 是一个兼顾语言新特性与工程化体验的版本语法层import type/export type让仅类型导入可被彻底擦除配合importsNotUsedAsValues精确控制导入代码生成ECMAScript#私有字段带来真正的运行时硬隐私export * as ns与顶层await紧跟 ECMAScript 2020 步伐es2020目标稳定化可选链、空值合并等新语法JS 文件支持层public/private/protected以及计划中的readonlyJSDoc 标签让.js文件也能获得类属性的访问性检查工具链层目录监听延迟化策略与watchOptions解决node_modules场景下的监听性能问题assumeChangesOnlyAffectDirectDependencies为超大规模代码库提供快而宽松的增量检查模式。这些特性从不同维度改善了类型安全、运行时健壮性与开发体验是理解后续 TypeScript 版本演进如verbatimModuleSyntax对类型导入体系的进一步统一的重要基础。相关配置细节可继续查阅仓库内的 编译器选项、tsconfig.json、配置 Watch以及完整的 发布说明索引。赞分享文档教程【免费下载链接】TypeScriptTypeScript 使用手册中文版翻译。http://www.typescriptlang.org项目地址https://gitcode.com/gh_mirrors/typ/TypeScript点击查看免费下载相关推荐TorchTitan 中的 DeepSeek V4稀疏注意力、头耦合混合HC与 MoE 哈希路由的完整实现与训练实战TorchTitan 中的 DeepSeek V4稀疏注意力、头耦合混合HC与 MoE 哈希路由的完整实现与训练实战 导读 本文围绕 TorchTitan文档教程Roc 编译器快照测试深度解析Opaque 类型模块字段引用私有顶层类型的可见性规则Roc 编译器快照测试深度解析Opaque 类型模块字段引用私有顶层类型的可见性规则 Roc 是一门快速、友好、函数式的语言其编译器以 Zig 实现并通过Pyrefly 的 Jupyter Notebook 类型检查从 .ipynb 解析到顶层 await 支持的完整机制Pyrefly 的 Jupyter Notebook 类型检查从 .ipynb 解析到顶层 await 支持的完整机制 本文以仓库中的端到端测试文档 test开发工具静态分析IDE代码质量上一篇下一代AI原生软件接口7步标准化方法论彻底改变自动化流程下一篇如何快速选择DBeaver插件面向开发者的完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考