JavaScript 在处理代码模块化方面有着悠久的历史,并采用了不同的方法。TypeScript 自 2012 年问世以来,已经实现了对许多这些格式的支持,但随着时间的推移,社区和 JavaScript 规范已经收敛到一种名为 ES 模块(或 ES6 模块)的格式。您可能将其称为 import
/export
语法。
ES 模块于 2015 年被添加到 JavaScript 规范中,到 2020 年,大多数 Web 浏览器和 JavaScript 运行时都广泛支持它。
为了集中精力,手册将涵盖 ES 模块及其流行的前身 CommonJS module.exports =
语法,您可以在参考部分的 模块 中找到有关其他模块模式的信息。
JavaScript 模块的定义方式
在 TypeScript 中,就像在 ECMAScript 2015 中一样,任何包含顶级 import
或 export
的文件都被视为模块。
相反,没有顶级导入或导出声明的文件被视为脚本,其内容在全局作用域中可用(因此也对模块可用)。
模块在它们自己的作用域内执行,而不是在全局作用域内。这意味着在模块中声明的变量、函数、类等在模块外部不可见,除非它们使用其中一种导出形式显式导出。相反,要使用从另一个模块导出的变量、函数、类、接口等,必须使用其中一种导入形式导入它们。
非模块
在我们开始之前,了解 TypeScript 将什么视为模块很重要。JavaScript 规范声明,任何没有 import
声明、export
或顶级 await
的 JavaScript 文件都应被视为脚本,而不是模块。
在脚本文件中,变量和类型被声明为在共享的全局作用域中,并且假设您将使用 outFile
编译器选项将多个输入文件合并到一个输出文件中,或者在您的 HTML 中使用多个 <script>
标签来加载这些文件(按正确的顺序!)。
如果你有一个文件目前没有import
或export
,但你想把它当作一个模块,添加一行
tsTry
export {};
这将把文件更改为一个不导出任何内容的模块。此语法适用于任何模块目标。
TypeScript 中的模块
在 TypeScript 中编写基于模块的代码时,需要考虑三个主要方面
- 语法: 我想使用什么语法来导入和导出内容?
- 模块解析: 模块名称(或路径)与磁盘上的文件之间是什么关系?
- 模块输出目标: 我的输出 JavaScript 模块应该是什么样子?
ES 模块语法
一个文件可以通过export default
声明一个主要导出
tsTry
// @filename: hello.tsexport default functionhelloWorld () {console .log ("Hello, world!");}
然后通过以下方式导入
tsTry
importhelloWorld from "./hello.js";helloWorld ();
除了默认导出之外,你还可以通过export
省略default
来导出多个变量和函数
tsTry
// @filename: maths.tsexport varpi = 3.14;export letsquareTwo = 1.41;export constphi = 1.61;export classRandomNumberGenerator {}export functionabsolute (num : number) {if (num < 0) returnnum * -1;returnnum ;}
这些可以在另一个文件中通过import
语法使用
tsTry
import {pi ,phi ,absolute } from "./maths.js";console .log (pi );constabsPhi =absolute (phi );
其他导入语法
可以使用类似 import {old as new}
的格式重命名导入。
tsTry
import {pi asπ } from "./maths.js";console .log (π );
可以将上述语法混合使用到单个 import
中。
tsTry
// @filename: maths.tsexport constpi = 3.14;export default classRandomNumberGenerator {}// @filename: app.tsimportRandomNumberGenerator , {pi asπ } from "./maths.js";RandomNumberGenerator ;console .log (π );
可以使用 * as name
将所有导出的对象放入单个命名空间。
tsTry
// @filename: app.tsimport * asmath from "./maths.js";console .log (math .pi );constpositivePhi =math .absolute (math .phi );
可以导入文件,但不将任何变量包含到当前模块中,方法是使用 import "./file"
。
tsTry
// @filename: app.tsimport "./maths.js";console .log ("3.14");
在这种情况下,import
不会执行任何操作。但是,maths.ts
中的所有代码都已执行,这可能会触发影响其他对象的副作用。
TypeScript 特定 ES 模块语法
类型可以使用与 JavaScript 值相同的语法导出和导入。
tsTry
// @filename: animal.tsexport typeCat = {breed : string;yearOfBirth : number };export interfaceDog {breeds : string[];yearOfBirth : number;}// @filename: app.tsimport {Cat ,Dog } from "./animal.js";typeAnimals =Cat |Dog ;
TypeScript 使用两种概念扩展了 import
语法,用于声明类型导入。
import type
这是一个只能导入类型的导入语句。
tsTry
// @filename: animal.tsexport type'createCatName' cannot be used as a value because it was imported using 'import type'.1361'createCatName' cannot be used as a value because it was imported using 'import type'.Cat = {breed : string;yearOfBirth : number };export typeDog = {breeds : string[];yearOfBirth : number };export constcreateCatName = () => "fluffy";// @filename: valid.tsimport type {Cat ,Dog } from "./animal.js";export typeAnimals =Cat |Dog ;// @filename: app.tsimport type {createCatName } from "./animal.js";constname =createCatName ();
内联 type
导入
TypeScript 4.5 还允许在单个导入前加上 `type` 前缀,以指示导入的引用是一个类型。
tsTry
// @filename: app.tsimport {createCatName , typeCat , typeDog } from "./animal.js";export typeAnimals =Cat |Dog ;constname =createCatName ();
这些功能共同使 Babel、swc 或 esbuild 等非 TypeScript 编译器能够识别哪些导入可以安全删除。
ES 模块语法与 CommonJS 行为
TypeScript 具有 ES 模块语法,它与 CommonJS 和 AMD 的 `require` 直接相关。使用 ES 模块的导入在大多数情况下与这些环境中的 `require` 相同,但这种语法确保您的 TypeScript 文件与 CommonJS 输出之间存在一对一的匹配关系。
tsTry
importfs = require("fs");constcode =fs .readFileSync ("hello.ts", "utf8");
您可以在 模块参考页面 中了解更多关于此语法的知识。
CommonJS 语法
CommonJS 是大多数 npm 模块使用的格式。即使您使用上面的 ES 模块语法编写代码,对 CommonJS 语法的工作原理有一个简单的了解也会帮助您更轻松地调试。
导出
标识符通过设置名为 `module` 的全局对象的 `exports` 属性来导出。
tsTry
functionabsolute (num : number) {if (num < 0) returnnum * -1;returnnum ;}module .exports = {pi : 3.14,squareTwo : 1.41,phi : 1.61,absolute ,};
然后,这些文件可以通过 require
语句导入
tsTry
constmaths =require ("./maths");maths .pi ;
或者,你可以使用 JavaScript 中的解构功能简化代码
tsTry
const {squareTwo } =require ("./maths");squareTwo ;
CommonJS 和 ES 模块互操作性
CommonJS 和 ES 模块在默认导入和模块命名空间对象导入之间的区别方面存在功能不匹配。TypeScript 有一个编译器标志,可以减少这两个不同约束集之间的摩擦,即 esModuleInterop
。
TypeScript 的模块解析选项
模块解析是指将 import
或 require
语句中的字符串转换为该字符串所指文件的过程。
TypeScript 包含两种解析策略:经典策略和 Node 策略。经典策略是编译器选项 module
不为 commonjs
时的默认策略,为了向后兼容而保留。Node 策略复制了 Node.js 在 CommonJS 模式下的工作方式,并额外检查了 .ts
和 .d.ts
文件。
许多 TSConfig 标志会影响 TypeScript 中的模块策略:moduleResolution
、baseUrl
、paths
、rootDirs
。
有关这些策略工作原理的完整详细信息,请参阅 模块解析 参考页面。
TypeScript 的模块输出选项
有两种选项会影响生成的 JavaScript 输出
您使用哪个 target
取决于您期望运行 TypeScript 代码的 JavaScript 运行时中可用的特性。这可能是:您支持的最旧的 Web 浏览器、您期望运行的最低版本的 Node.js,或者可能是来自您运行时(例如 Electron)的独特约束。
模块之间的所有通信都通过模块加载器进行,编译器选项 module
决定使用哪个模块加载器。在运行时,模块加载器负责在执行模块之前找到并执行该模块的所有依赖项。
例如,以下是一个使用 ES 模块语法的 TypeScript 文件,展示了 module
的一些不同选项
tsTry
import {valueOfPi } from "./constants.js";export consttwoPi =valueOfPi * 2;
ES2020
tsTry
import { valueOfPi } from "./constants.js";export const twoPi = valueOfPi * 2;
CommonJS
tsTry
"use strict";Object.defineProperty(exports, "__esModule", { value: true });exports.twoPi = void 0;const constants_js_1 = require("./constants.js");exports.twoPi = constants_js_1.valueOfPi * 2;
UMD
tsTry
(function (factory) {if (typeof module === "object" && typeof module.exports === "object") {var v = factory(require, exports);if (v !== undefined) module.exports = v;}else if (typeof define === "function" && define.amd) {define(["require", "exports", "./constants.js"], factory);}})(function (require, exports) {"use strict";Object.defineProperty(exports, "__esModule", { value: true });exports.twoPi = void 0;const constants_js_1 = require("./constants.js");exports.twoPi = constants_js_1.valueOfPi * 2;});
请注意,ES2020 与原始的
index.ts
实际上是相同的。
您可以在 module
的 TSConfig 参考 中查看所有可用的选项及其生成的 JavaScript 代码。
TypeScript 命名空间
TypeScript 拥有自己的模块格式,称为 namespaces
,它早于 ES 模块标准。这种语法在创建复杂的定义文件方面有很多有用的功能,并且在 DefinitelyTyped 中仍然被积极使用。虽然没有被弃用,但命名空间中的大多数功能都存在于 ES 模块中,我们建议您使用它来与 JavaScript 的方向保持一致。您可以在 命名空间参考页面 中了解更多关于命名空间的信息。