模块

JavaScript 在模块化代码的处理方式上有着悠久的历史。自 2012 年以来,TypeScript 就已经对许多这些格式提供了支持,但随着时间的推移,社区和 JavaScript 规范已经趋向于一种被称为 ES 模块(或 ES6 模块)的格式。你可能会以 import/export 语法的形式认识它。

🌐 JavaScript has a long history of different ways to handle modularizing code. Having been around since 2012, TypeScript has implemented support for a lot of these formats, but over time the community and the JavaScript specification has converged on a format called ES Modules (or ES6 modules). You might know it as the import/export syntax.

ES Modules 于 2015 年被添加到 JavaScript 规范中,到 2020 年在大多数 Web 浏览器和 JavaScript 运行时得到广泛支持。

🌐 ES Modules was added to the JavaScript spec in 2015, and by 2020 had broad support in most web browsers and JavaScript runtimes.

为了便于理解,本手册将涵盖 ES 模块及其流行的前身 CommonJS module.exports = 语法,其他模块模式的信息可以在参考部分的 Modules 中找到。

🌐 For focus, the handbook will cover both ES Modules and its popular pre-cursor CommonJS module.exports = syntax, and you can find information about the other module patterns in the reference section under Modules.

JavaScript 模块是如何定义的

🌐 How JavaScript Modules are Defined

在 TypeScript 中,就像在 ECMAScript 2015 中一样,任何包含顶层 importexport 的文件都被视为模块。

🌐 In TypeScript, just as in ECMAScript 2015, any file containing a top-level import or export is considered a module.

相反,没有任何顶层导入或导出声明的文件被视为其内容在全局作用域内可用的脚本(因此也可用于模块)。

🌐 Conversely, a file without any top-level import or export declarations is treated as a script whose contents are available in the global scope (and therefore to modules as well).

模块在其自身的作用域内执行,而不是在全局作用域中执行。这意味着在模块中声明的变量、函数、类等在模块外部是不可见的,除非它们被显式地使用某种导出形式导出。相反,要使用从不同模块导出的变量、函数、类、接口等,必须使用某种导入形式将其导入。

🌐 Modules are executed within their own scope, not in the global scope. This means that variables, functions, classes, etc. declared in a module are not visible outside the module unless they are explicitly exported using one of the export forms. Conversely, to consume a variable, function, class, interface, etc. exported from a different module, it has to be imported using one of the import forms.

非模块

🌐 Non-modules

在我们开始之前,了解 TypeScript 认为什么是模块非常重要。JavaScript 规范声明,任何没有 import 声明、export 或顶层 await 的 JavaScript 文件都应该被视为脚本,而不是模块。

🌐 Before we start, it’s important to understand what TypeScript considers a module. The JavaScript specification declares that any JavaScript files without an import declaration, export, or top-level await should be considered a script and not a module.

在脚本文件中,变量和类型被声明在共享的全局作用域中,并且假设你要么使用 outFile 编译选项将多个输入文件合并为一个输出文件,要么在 HTML 中使用多个 <script> 标签来加载这些文件(顺序必须正确!)。

🌐 Inside a script file variables and types are declared to be in the shared global scope, and it’s assumed that you’ll either use the outFile compiler option to join multiple input files into one output file, or use multiple <script> tags in your HTML to load these files (in the correct order!).

如果你的文件当前没有任何 importexport,但你希望它被视为模块,请添加以下行:

🌐 If you have a file that doesn’t currently have any imports or exports, but you want to be treated as a module, add the line:

ts
export {};
Try

这将把文件更改为不导出任何内容的模块。无论你的模块目标是什么,这种语法都能工作。

🌐 which will change the file to be a module exporting nothing. This syntax works regardless of your module target.

TypeScript 中的模块

🌐 Modules in TypeScript

补充阅读:
Impatient JS(模块)
MDN: JavaScript 模块

在 TypeScript 中编写基于模块的代码时,需要考虑三件事:

🌐 There are three main things to consider when writing module-based code in TypeScript:

  • 语法:我想使用什么语法来导入和导出内容?
  • 模块解析:模块名称(或路径)与磁盘上的文件之间有什么关系?
  • 模块输出目标:我的输出 JavaScript 模块应该是什么样的?

ES 模块语法

🌐 ES Module Syntax

一个文件可以通过 export default 声明一个主要导出:

🌐 A file can declare a main export via export default:

ts
// @filename: hello.ts
export default function helloWorld() {
console.log("Hello, world!");
}
Try

然后通过以下方式导入:

🌐 This is then imported via:

ts
import helloWorld from "./hello.js";
helloWorld();
Try

除了默认导出之外,你还可以通过 export 导出多个变量和函数,只需省略 default

🌐 In addition to the default export, you can have more than one export of variables and functions via the export by omitting default:

ts
// @filename: maths.ts
export var pi = 3.14;
export let squareTwo = 1.41;
export const phi = 1.61;
 
export class RandomNumberGenerator {}
 
export function absolute(num: number) {
if (num < 0) return num * -1;
return num;
}
Try

可以通过 import 语法在另一个文件中使用这些内容:

🌐 These can be used in another file via the import syntax:

ts
import { pi, phi, absolute } from "./maths.js";
 
console.log(pi);
const absPhi = absolute(phi);
const absPhi: number
Try

额外的导入语法

🌐 Additional Import Syntax

可以使用类似 import {old as new} 的格式重命名导入项:

🌐 An import can be renamed using a format like import {old as new}:

ts
import { pi as π } from "./maths.js";
 
console.log(π);
(alias) var π: number import π
Try

你可以将上述语法混合搭配到一个 import 中:

🌐 You can mix and match the above syntax into a single import:

ts
// @filename: maths.ts
export const pi = 3.14;
export default class RandomNumberGenerator {}
 
// @filename: app.ts
import RandomNumberGenerator, { pi as π } from "./maths.js";
 
RandomNumberGenerator;
(alias) class RandomNumberGenerator import RandomNumberGenerator
 
console.log(π);
(alias) const π: 3.14 import π
Try

你可以使用 * as name 将所有导出的对象放入一个命名空间中:

🌐 You can take all of the exported objects and put them into a single namespace using * as name:

ts
// @filename: app.ts
import * as math from "./maths.js";
 
console.log(math.pi);
const positivePhi = math.absolute(math.phi);
const positivePhi: number
Try

你可以通过 import "./file" 导入一个文件,而不将任何变量包含到你当前的模块中:

🌐 You can import a file and not include any variables into your current module via import "./file":

ts
// @filename: app.ts
import "./maths.js";
 
console.log("3.14");
Try

在这种情况下,import 什么也不做。然而,maths.ts 中的所有代码都会被执行,这可能触发影响其他对象的副作用。

🌐 In this case, the import does nothing. However, all of the code in maths.ts was evaluated, which could trigger side-effects which affect other objects.

TypeScript 特定的 ES 模块语法

🌐 TypeScript Specific ES Module Syntax

可以使用与 JavaScript 值相同的语法导出和导入类型:

🌐 Types can be exported and imported using the same syntax as JavaScript values:

ts
// @filename: animal.ts
export type Cat = { breed: string; yearOfBirth: number };
 
export interface Dog {
breeds: string[];
yearOfBirth: number;
}
 
// @filename: app.ts
import { Cat, Dog } from "./animal.js";
type Animals = Cat | Dog;
Try

TypeScript 在 import 语法中扩展了两个用于声明类型导入的概念:

🌐 TypeScript has extended the import syntax with two concepts for declaring an import of a type:

import type

哪一个是只能导入类型的导入语句:

🌐 Which is an import statement which can only import types:

ts
// @filename: animal.ts
export type Cat = { breed: string; yearOfBirth: number };
export type Dog = { breeds: string[]; yearOfBirth: number };
export const createCatName = () => "fluffy";
 
// @filename: valid.ts
import type { Cat, Dog } from "./animal.js";
export type Animals = Cat | Dog;
 
// @filename: app.ts
import type { createCatName } from "./animal.js";
const name = createCatName();
'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'.
Try
内联 type 导入

🌐 Inline type imports

TypeScript 4.5 还允许对单个导入前加上 type 前缀,以表示导入的引用是一个类型:

🌐 TypeScript 4.5 also allows for individual imports to be prefixed with type to indicate that the imported reference is a type:

ts
// @filename: app.ts
import { createCatName, type Cat, type Dog } from "./animal.js";
 
export type Animals = Cat | Dog;
const name = createCatName();
Try

这些一起允许像 Babel、swc 或 esbuild 这样的非 TypeScript 转译器知道可以安全地删除哪些导入。

🌐 Together these allow a non-TypeScript transpiler like Babel, swc or esbuild to know what imports can be safely removed.

具有 CommonJS 行为的 ES 模块语法

🌐 ES Module Syntax with CommonJS Behavior

TypeScript 有 ES 模块语法,它与 CommonJS 和 AMD 的 require 直接 对应。使用 ES 模块进行导入在 大多数情况下 与这些环境中的 require 相同,但这种语法确保在 TypeScript 文件中与 CommonJS 输出有一对一的匹配:

🌐 TypeScript has ES Module syntax which directly correlates to a CommonJS and AMD require. Imports using ES Module are for most cases the same as the require from those environments, but this syntax ensures you have a 1 to 1 match in your TypeScript file with the CommonJS output:

ts
import fs = require("fs");
const code = fs.readFileSync("hello.ts", "utf8");
Try

你可以在模块参考页面了解有关此语法的更多信息。

🌐 You can learn more about this syntax in the modules reference page.

CommonJS 语法

🌐 CommonJS Syntax

CommonJS 是大多数 npm 模块使用的格式。即使你使用上面提到的 ES 模块语法进行编写,了解 CommonJS 语法的基本用法也会帮助你更轻松地进行调试。

🌐 CommonJS is the format which most modules on npm are delivered in. Even if you are writing using the ES Modules syntax above, having a brief understanding of how CommonJS syntax works will help you debug easier.

导出

🌐 Exporting

标识符通过在名为 module 的全局对象上设置 exports 属性来导出。

🌐 Identifiers are exported via setting the exports property on a global called module.

ts
function absolute(num: number) {
if (num < 0) return num * -1;
return num;
}
 
module.exports = {
pi: 3.14,
squareTwo: 1.41,
phi: 1.61,
absolute,
};
Try

然后可以通过 require 语句导入这些文件:

🌐 Then these files can be imported via a require statement:

ts
const maths = require("./maths");
maths.pi;
any
Try

或者你可以使用 JavaScript 中的解构功能进行一些简化:

🌐 Or you can simplify a bit using the destructuring feature in JavaScript:

ts
const { squareTwo } = require("./maths");
squareTwo;
const squareTwo: any
Try

CommonJS 与 ES 模块互操作

🌐 CommonJS and ES Modules interop

在 CommonJS 和 ES 模块之间的功能存在不匹配,涉及默认导入与模块命名空间对象导入之间的区别。TypeScript 有一个编译器标志,可以通过 esModuleInterop 来减少两组不同约束之间的冲突。

🌐 There is a mis-match in features between CommonJS and ES Modules regarding the distinction between a default import and a module namespace object import. TypeScript has a compiler flag to reduce the friction between the two different sets of constraints with esModuleInterop.

TypeScript 的模块解析选项

🌐 TypeScript’s Module Resolution Options

模块解析是从 importrequire 语句中获取字符串,并确定该字符串所指的文件的过程。

🌐 Module resolution is the process of taking a string from the import or require statement, and determining what file that string refers to.

TypeScript 包含两种解析策略:Classic 和 Node。Classic 是默认策略,当编译器选项 module 不是 commonjs 时使用,主要是为了向后兼容。Node 策略模拟了 Node.js 在 CommonJS 模式下的工作方式,并增加了对 .ts.d.ts 的额外检查。

🌐 TypeScript includes two resolution strategies: Classic and Node. Classic, the default when the compiler option module is not commonjs, is included for backwards compatibility. The Node strategy replicates how Node.js works in CommonJS mode, with additional checks for .ts and .d.ts.

在 TypeScript 中,有许多 TSConfig 标志会影响模块策略:moduleResolutionbaseUrlpathsrootDirs

🌐 There are many TSConfig flags which influence the module strategy within TypeScript: moduleResolution, baseUrl, paths, rootDirs.

有关这些策略如何运作的详细信息,你可以查阅 模块解析 参考页面。

🌐 For the full details on how these strategies work, you can consult the Module Resolution reference page.

TypeScript 的模块输出选项

🌐 TypeScript’s Module Output Options

有两个选项会影响触发的 JavaScript 输出:

🌐 There are two options which affect the emitted JavaScript output:

  • target 决定哪些 JS 特性会被降级(转换为可以在较旧的 JavaScript 运行时中运行)以及哪些特性会保持不变
  • module 决定了模块之间如何相互交互的代码

你使用哪个 target 取决于你期望运行 TypeScript 代码的 JavaScript 运行环境所提供的功能。这可能是:你支持的最老的网页浏览器、你期望运行的最低版本 Node.js,或者可能来自你运行环境的独特限制——例如 Electron。

🌐 Which target you use is determined by the features available in the JavaScript runtime you expect to run the TypeScript code in. That could be: the oldest web browser you support, the lowest version of Node.js you expect to run on or could come from unique constraints from your runtime - like Electron for example.

模块之间的所有通信都是通过模块加载器进行的,编译器选项 module 决定使用哪一个。运行时,模块加载器负责在执行模块之前定位并执行模块的所有依赖。

🌐 All communication between modules happens via a module loader, the compiler option module determines which one is used. At runtime the module loader is responsible for locating and executing all dependencies of a module before executing it.

例如,这里有一个使用 ES 模块语法的 TypeScript 文件,展示了 module 的几种不同选项:

🌐 For example, here is a TypeScript file using ES Modules syntax, showcasing a few different options for module:

ts
import { valueOfPi } from "./constants.js";
 
export const twoPi = valueOfPi * 2;
Try

ES2020

ts
import { valueOfPi } from "./constants.js";
export const twoPi = valueOfPi * 2;
 
Try

CommonJS

ts
"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;
 
Try

UMD

ts
(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;
});
 
Try

注意,ES2020 实际上与原始的 index.ts 是相同的。

你可以在 module 的 TSConfig 参考 中查看所有可用选项以及它们生成的 JavaScript 代码样式。

🌐 You can see all of the available options and what their emitted JavaScript code looks like in the TSConfig Reference for module.

TypeScript 命名空间

🌐 TypeScript namespaces

TypeScript 有其自身的模块格式,称为 namespaces,它比 ES 模块标准更早出现。这种语法在创建复杂的定义文件时有许多有用的功能,并且仍在 DefinitelyTyped 中被积极使用。虽然并未被废弃,但命名空间中的大多数功能在 ES 模块中也存在,我们建议你使用 ES 模块以符合 JavaScript 的发展方向。你可以在 命名空间参考页面 了解更多关于命名空间的内容。

🌐 TypeScript has its own module format called namespaces which pre-dates the ES Modules standard. This syntax has a lot of useful features for creating complex definition files, and still sees active use in DefinitelyTyped. While not deprecated, the majority of the features in namespaces exist in ES Modules and we recommend you use that to align with JavaScript’s direction. You can learn more about namespaces in the namespaces reference page.