模块

模块化编程是一种软件设计方法,它强调将程序按照功能划分为独立可交互的模块。一个模块是一段可重用的代码,它将功能的实现细节封装在模块内部。模块也是一种组织代码的方式。一个模块可以声明对其他模块的依赖,且模块之间只能通过模块的公共API进行交互。在新的工程或代码中,应该优先使用模块来组织代码,因为模块提供了更好的封装性和可重用性。

模块简史

自 1996 年 JavaScript 诞生到 2015 年 ECMAScript 2015 发布,在将近 20 年的时间里 JavaScript 语言始终缺少原生的模块功能。在这 20 年间,社区的开发者们设计了多种模块系统来帮助进行 JavaScript 模块化编程。其中较为知名的模块系统有以下几种:

  • CommonJS 模块

  • AMD 模块

  • UMD 模块

CommonJS

CommonJS 是一个主要用于服务器端 JavaScript 程序的模块系统。CommonJS 使用 require 语句来声明对其他模块的依赖,同时使用 exports 语句来导出当前模块内的声明。CommonJS 的典型应用场景是在 Node.js 程序中。在 Node.js 中,每一个文件都会被视为一个模块。

例如,有如下目录结构的工程:

C:\app
|-- index.js
`-- utils.js

此例中,utils.jsindex.js 是两个 CommonJS 模块文件。其中,index.js 模块文件声明了对 utils.js 模块文件的依赖。

utils.js 文件的内容如下:

exports.add = function(x, y) {
    return x + y;
};

index.js 文件的内容如下:

const utils = require('./utils');

const total = utils.add(1, 2);
console.log(total);

AMD

CommonJS 模块系统在服务器端 JavaScript 程序中取得了成功,但无法给浏览器端 JavaScript 程序带来帮助。主要原因有以下两点:

  • 浏览器环境中的 JavaScript 引擎不支持 CommonJS 模块,因此无法直接运行使用了 CommonJS 模块的代码。

  • CommonJS 模块采用同步的方式加载模块文件,这种加载方式不适用于浏览器环境。因为在浏览器中同步地加载模块文件会阻塞用户操作,从而带来不好的用户体验。

基于以上原因,CommonJS 的设计者又进一步设计了适用于浏览器环境的 AMD 模块系统。AMD 是 Asynchronous Module Definition 的缩写,表示异步模块定义。AMD 模块系统不是将一个文件作为一个模块,而是使用特殊的 define 函数来注册一个模块。因此,在一个文件中允许同时定义多个模块。AMD 模块系统中也提供了 require 函数用来声明对其他模块的依赖,同时还提供了 exports 语句用来导出当前模块内的声明。

例如,有如下目录结构的工程:

C:\app
|-- index.js
`-- utils.js

utils.js 文件的内容如下:

define(['require', 'exports'], function(require, exports) {
    function add(x, y) {
        return x + y;
    }
    exports.add = add;
});

index.js 文件的内容如下:

define(
    ['require', 'exports', './utils'],
    function(require, exports, utils) {
        var total = utils.add(1, 2);
        console.log(total);
    }
);

此例中,在 utils.jsindex.js 文件中分别定义了两个 AMD 模块。其中,index.js 文件中的模块文件声明了对 utils.js 文件中的模块的依赖。

UMD

虽然 CommonJS 模块和 AMD 模块有着紧密的内在联系和相似的定义方式,但是两者不能互换使用。CommonJS 模块不能在浏览器中使用,AMD 模块也不能在 Node.js 中使用。如果一个功能模块既要在浏览器中使用也要在 Node.js 环境中使用,就需要分别使用 CommonJS 模块和 AMD 模块的格式编写两次。

UMD 模块的出现解决了这个问题。UMD 是 Universal Module Definition 的缩写,表示通用模块定义。一个 UMD 模块既可以在浏览器中使用,也可以在 Node.js 中使用。UMD 模块是基于 AMD 模块的定义,并且针对 CommonJS 模块定义进行了适配。因此,编写 UMD 模块会稍显复杂。

例如,有如下目录结构的工程:

C:\app
|-- index.js
`-- utils.js

utils.js 文件的内容如下:

(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'], factory);
    }
})(function(require, exports) {
    function add(x, y) {
        return x + y;
    }
    exports.add = add;
});

index.js 文件的内容如下:

(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', './utils'], factory);
    }
})(function(require, exports) {
    var utils_1 = require('./utils');
    var total = utils_1.add(1, 2);
    console.log(total);
});

此例中,在 utils.jsindex.js 文件中分别定义了两个 UMD 模块。其中,index.js 文件中的模块文件声明了对 utils.js 文件中的模块的依赖。

ESM

在经过了将近 10 年的标准化设计后,JavaScript 语言的官方模块标准终于确定并随着 ECMAScript 2015 一同发布。它就是 ECMAScript 模块,简称为 ES 模块或 ESMECMAScript 模块是正式的语言内置模块标准,而前面介绍的 CommonJSAMD 等都属于非官方模块标准。在未来,标准的 ECMAScript 模块将能够在任何 JavaScript 运行环境中使用,例如浏览器环境和服务器端环境等。实际上,在最新版本的 ChromeFirefox 等浏览器上以及 Node.js 环境中已经能够支持 ECMAScript 模块。ECMAScript 模块使用 importexport 等关键字来定义。

例如,有如下目录结构的工程:

C:\app
|-- index.js
`-- utils.js

utils.js 文件的内容如下:

export function add(x: number, y: number) {
    return x + y;
}

index.js 文件的内容如下:

import { add } from './utils';

const total = add(1, 2);

此例中,utils.jsindex.js 是两个 ECMAScript 模块文件。其中,index.js 模块文件声明了对 utils.js 模块文件的依赖。

ECMAScript模块

ECMAScript 模块是 JavaScript 语言的标准模块,因此 TypeScript 也支持 ECMAScript 模块。在后面的介绍中,我们将 ECMAScript 模块简称为模块。

每个模块都拥有独立的模块作用域,模块中的代码在其独立的作用域内运行,而不会影响模块外的作用域(有副作用的模块除外,后文将详细介绍)。模块通过 import 语句来声明对其他模块的依赖;同时,通过 export 语句将模块内的声明公开给其他模块使用。

模块不是使用类似于 module 的某个关键字来定义,而是以文件为单位。一个模块对应一个文件,同时一个文件也只能表示一个模块,两者是一对一的关系。若一个 TypeScript 文件中带有顶层的 importexport 语句,那么该文件就是一个模块,术语为 Module。若一个 TypeScript 文件中既不包含 import 语句,也不包含 export 语句,那么该文件称作脚本,术语为 Script。脚本中的代码全部是全局代码,它直接存在于全局作用域中。因此,模块中的代码能够访问脚本中的代码,因为在模块作用域中能够访问外层的全局作用域。

模块导出

默认情况下,在模块内部的声明不允许在模块外部访问。若想将模块内部的声明开放给模块外部访问,则需要使用模块导出语句将模块内的声明导出。模块导出语句包含以下两类:

  • 命名模块导出。

  • 默认模块导出。

命名模块导出

命名模块导出使用自定义的标识符名来区分导出声明。在一个模块中,可以同时存在多个命名模块导出。在常规的声明语句中添加 export 关键字,即可定义命名模块导出。

导出变量声明示例如下:

export var a = 0;

export let b = 0;

export const c = 0;

导出函数声明示例如下:

export function f() {}

导出类声明示例如下:

export class C {}

导出接口声明示例如下:

export interface I {}

导出类型别名示例如下:

export type Numeric = number | bigint;

命名模块导出列表

进行命名模块导出时,一次只能导出一个声明,而命名模块导出列表能够一次性导出多个声明。命名模块导出列表使用 export 关键字和一对大括号将所有导出的声明名称包含在内。示例如下:

function f0() {}
function f1() {}

export { f0, f1 };

此例中,第 4 行是命名模块导出列表语句,它同时导出了函数声明 f0f1

在一个模块中,可以同时存在多个命名模块导出列表语句。示例如下:

const a = 0;
const b = 0;

export { a, b };

function f0() {}
function f1() {}

export { f0, f1 };

命名模块导出语句和命名模块导出列表语句也可以同时使用。示例如下:

export const a = 0;

function f0() {}
function f1() {}

export { f0, f1 };

默认模块导出

为了与现有的 CommonJS 模块和 AMD 模块兼容,ECMAScript 模块提供了默认模块导出的功能。对于一个 CommonJS 模块或 AMD 模块来讲,模块中的 exports 对象就相当于默认模块导出。

默认模块导出是一种特殊形式的模块导出,它等同于名字为 default 的命名模块导出。因此,一个模块中只允许存在一个默认模块导出。默认模块导出使用 export default 关键字来定义。

默认导出函数声明示例如下:

export default function f() {}

默认导出类声明示例如下:

export default class C {}

因为默认模块导出不依赖于声明的名字而是统一使用 default 作为导出名,因此默认模块导出可以导出匿名的函数和类等。

默认导出匿名函数示例如下:

export default function() {}

默认导出匿名类示例如下:

export default class {}

默认导出任意表达式的值示例如下:

export default 0;

由于默认模块导出相当于名为 default 的命名模块导出,因此,默认模块导出也可以写为如下形式:

function f() {}

export { f as default };

此例中,as 关键字的作用是重命名模块导出,它将 f 重命名为 default。我们将在后文中详细介绍模块导出的重命名。此例中的命名模块导出列表等同于如下的默认模块导出语句:

export default function f() {}

聚合模块

聚合模块是指将其他模块的模块导出作为当前模块的模块导出。聚合模块使用 export …​ from …​ 语法并包含以下形式:

  • 从模块 mod 中选择部分模块导出作为当前模块的模块导出。示例如下:

    export { a, b, c } from 'mod';
  • 从模块 mod 中选择默认模块导出作为当前模块的默认模块导出。默认模块导出相当于名为 default 的命名模块导出。示例如下:

    export { default } from 'mod';
  • 从模块 mod 中选择某个非默认模块导出作为当前模块的默认模块导出。默认模块导出相当于名为 default 的命名模块导出。示例如下:

    export { a as default } from 'mod';
  • 从模块 mod 中选择所有非默认模块导出作为当前模块的模块导出。示例如下:

    export * from 'mod';
  • 从模块 mod 中选择所有非默认模块导出,并以 ns 为名作为当前模块的模块导出。示例如下:

    export * as ns from "mod";

需要注意的是,在聚合模块时不会引入任何本地声明。例如,下例从模块 mod 中重新导出了声明 a,但是在当前模块中是不允许使用声明 a 的,因为没有导入声明 a。示例如下:

export { a } from 'mod';

console.log(a);
//          ~
//          编译错误!找不到名字 "a"

模块导入

如果想要使用一个模块的导出声明,则需要使用 import 语句来导入它。

导入命名模块导出

对于一个模块的命名模块导出,可以通过其导出的名称来导入它,具体语法如下所示:

import { a, b, c } from 'mod';

在该语法中,import 关键字后面的大括号中列出了 mod 模块中的命名模块导出;from 关键字的后面是模块名,模块名不包含文件扩展名,如 .ts

例如,有如下目录结构的工程:

C:\app
|-- index.ts
`-- utils.ts

utils.ts 文件的内容如下:

export let a = 0;
export let b = 0;
export let c = 0;

index.ts 文件的内容如下:

import { a, b } from './utils';

console.log(a);
console.log(b);

此例中,在 index.ts 模块中导入了 utils.ts 模块中的导出变量声明 ab

导入整个模块

我们可以将整个模块一次性地导入,语法如下所示:

import * as ns from 'mod';

在该语法中,from 后面是要导入的模块名,它将模块 mod 中的所有命名模块导出导入到对象 ns 中。

例如,有如下目录结构的工程:

C:\app
|-- index.ts
`-- utils.ts

utils.ts 文件的内容如下:

export let a = 0;
export let b = 0;
export let c = 0;

index.ts 文件的内容如下:

import * as utils from './utils';

console.log(utils.a);
console.log(utils.b);

此例中,在 index.ts 模块中将 utils.ts 模块中的命名模块导出 ab 导入了对象 utils 中,然后通过访问 utils 对象的属性来访问 utils.ts 模块的命名模块导出。

导入默认模块导出

导入默认模块导出需要使用如下语法:

import modDefault from 'mod';

在该语法中,modDefault 可以为任意标识符名,表示导入的默认模块导出在当前模块中所绑定的标识符。在当前模块中,将使用 modDefault 这个名字来访问 mod 模块中的默认模块导出。

例如,有如下目录结构的工程:

C:\app
|-- index.ts
`-- utils.ts

utils.ts 文件的内容如下:

export default function() {
    console.log(0);
}

index.ts 文件的内容如下:

import utils from './utils';

utils();

此例中,在 index.ts 模块中导入了 utils.ts 模块中的默认模块导出并将其绑定到标识符 utils。在 index.ts 模块中,标识符 utils 表示 utils.ts 模块中默认导出的函数声明。

空导入

空导入语句不会导入任何模块导出,它只是执行模块内的代码。空导入的用途是导入模块的副作用。

在计算机科学中,副作用 指的是某个操作会对外部环境产生影响。例如,有一个获取时间的函数,如果该函数除了会返回当前时间,同时还会修改操作系统的时间设置,那么我们可以说该函数具有副作用。对于模块来讲,模块有其独立的模块作用域,但是在模块作用域中也能够访问并修改全局作用域中的声明。有些模块从设计上就是用来与全局作用域进行交互的,如监听全局事件或设置某个全局变量等。除此之外,应尽量保持模块与外部环境的隔离,将模块的实现封闭在模块内部,并通过导入和导出语句与模块外部进行交互。

空导入的语法如下所示:

import 'mod';

例如,有如下目录结构的工程:

C:\app
|-- index.ts
`-- utils.ts

utils.ts 文件的内容如下:

globalThis.mode = 'dev';

index.ts 文件的内容如下:

import './utils';

console.log(globalThis.mode);

此例中,使用空导入语句导入了 utils.ts 模块,这会执行 utils.ts 文件中的代码并设置全局作用域中 mode 属性的值。因此,在 index.ts 模块中能够读取并打印全局作用域中 mode 属性的值。

重命名模块导入到导出

为了解决模块导入和导出的命名冲突问题,ECMAScript 模块允许重命名模块的导入和导出声明。重命名模块导入和导出通过 as 关键字来定义。

重命名模块导出

重命名模块导出的语法如下所示:

export { oldName as newName };

在该语法中,将导出模块内的 oldName 声明,并将其重命名为 newName。在其他模块中需要使用 newName 这个名字来访问该模块中的 oldName 声明。

例如,有如下目录结构的工程:

C:\app
|-- index.ts
`-- utils.ts

utils.ts 文件的内容如下:

const a = 0;

export { a as x };

在该模块中,我们导出了常量声明 a,并将其重命名为 x

index.ts 文件的内容如下:

import { x } from './utils';

console.log(x);

在该模块中,我们使用新的名字 x 来导入 utils.ts 模块中的常量声明 a

重命名聚合模块

重命名聚合模块的语法如下所示:

export { oldName as newName } from "mod";

在该语法中,将导出 mod 模块内的 oldName 声明,并将其重命名为 newName

例如,有如下目录结构的工程:

C:\app
|-- index.ts
`-- utils.ts

utils.ts 文件的内容如下:

export const a = 0;

index.ts 文件的内容如下:

export { a as utilsA } from './utils';

export const b = 0;

在该模块中,我们重新导出了 utils.ts 模块中的声明 a,并将其重命名为 utilsA

重命名模块导入

重命名模块导入的语法如下所示:

import { oldName as newName } from "mod";

在该语法中,将导入 mod 模块内的 oldName 声明,并重命名为 newName。在当前模块中需要使用 newName 这个名字来访问 mod 模块中的 oldName 声明。

例如,有如下目录结构的工程:

C:\app
|-- index.ts
`-- utils.ts

utils.ts 文件的内容如下:

export const a = 0;

index.ts 文件的内容如下:

import { a as utilsA } from './utils';

console.log(utilsA);

在该模块文件中,我们导入了 utils.ts 模块中的声明 a,并将其重命名为 utilsA。在 index.ts 模块中,需要使用 utilsA 来访问 utils.ts 模块中的声明 a

针对类型的模块导入与导出

我们知道类和枚举既能表示一个值也能表示一种类型。在使用 importexport 语句来导入和导出类和枚举时,会同时导入和导出它们所表示的值和类型。因此,在代码中我们可以将导入的类和枚举同时用作值和类型。在 TypeScript 3.8 版本中,引入了只针对类型的导入导出语句。当在类和枚举上使用针对类型的导入导出语句时,只会导入和导出类和枚举所表示的类型,而不会导入和导出它们表示的值。延伸来看,在变量声明、函数声明上使用针对类型的导入导出语句时只会导入导出变量类型和函数类型,而不会导入导出变量的值和函数值。

背景介绍

TypeScriptJavaScript 添加了额外的静态类型,与类型相关的代码在编译生成 JavaScript 代码时会被完全删除,因为 JavaScript 本身并不支持静态类型。例如,我们在 TypeScript 程序中定义了一个接口,那么该接口声明在编译生成 JavaScript 时会被直接删除。该规则同样适用于模块导入导出语句。在默认情况下,如果模块的导入导出语句满足如下条件,那么在编译生成 JavaScript 时编译器会删除相应的导入导出语句,具体的条件如下:

  • 模块导入或导出的标识符仅被用在类型的位置上。

  • 模块导入或导出的标识符没有被用在表达式的位置上,即没有作为一个值使用。

例如,有如下目录结构的工程:

C:\app
|-- index.ts
`-- utils.ts

utils.ts 文件的内容如下:

export const a = 0;

export interface Point {
  x: number;
  y: number;
}

index.ts 文件的内容如下:

import { Point } from './utils';

const p: Point = { x: 0, y: 0 };

使用 tsc 命令来编译以上两个文件,将生成 utils.jsindex.js 文件。

生成的 utils.js 文件的内容为:

export const a = 0;

生成的 index.js 文件的内容为:

const p = { x: 0, y: 0 };

此例中,utils.ts 模块导出的 Point 接口在生成的 utils.js 文件中被删除,因为接口只能表示一种类型。在 index.ts 文件中,导入 Point 接口的语句在生成的 index.js 文件中也被删除了,因为在 index.tsPoint 作为类型来使用,它不影响生成的 JavaScript 代码。

虽然在大部分情况下,编译器删除针对类型的导入导出语句不会影响生成的 JavaScript 代码,但有时候也会给开发者带来困扰。一个典型的例子是使用了带有副作用的模块。如果一个模块只从带有副作用的模块中导入了类型,那么这条导入语句将会被编译器删除。因此,带有副作用的模块代码将不会被执行,这有可能不是期望的行为。

例如,有如下目录结构的工程:

C:\app
|-- index.ts
`-- utils.ts

utils.ts 文件的内容如下:

globalThis.mode = 'dev';

export interface Point {
  x: number;
  y: number;
}

index.ts 文件的内容如下:

import { Point } from './utils';

const p: Point = { x: 0, y: 0 };

if (globalThis.mode === 'dev') {
  console.log(p);
}

由于在 index.ts 文件中只导入了 utils.ts 模块中的接口类型,因此在编译生成的 JavaScript 文件中会删除导入 utils.ts 模块的语句,生成的 index.js 文件的内容如下:

const p = { x: 0, y: 0 };
if (globalThis.mode === 'dev') {
    console.log(p);
}

而事实上,此例中的 index.ts 模块依赖于 utils.ts 模块中的副作用,即设置全局的 mode 属性。因此,期望生成的 index.js 文件的内容如下:

import './utils';
const p = { x: 0, y: 0 };
if (globalThis.mode === 'dev') {
    console.log(p);
}

在 TypeScript 3.8 版本中,引入了只针对类型的模块导入导出语句以及 --importsNot-UsedAsValues 编译选项来帮助缓解上述问题。

导入与导出类型

总的来说,针对类型的模块导入导出语法是在前面介绍的模块导入导出语法中添加 type 关键字。

从模块中导出类型使用 export type 关键字,具体语法如下所示:

export type { Type }

export type { Type } from 'mod';

该语法中,Type 表示类型名。

从模块中导入默认模块导出类型的语法如下所示:

import type DefaultType from 'mod';

该语法中,DefaultType 可以为任意的标识符名,表示导入的默认模块导出类型在当前模块中所绑定的标识符。在当前模块中,将使用 DefaultType 这个名字来访问 mod 模块中的默认模块导出类型。

从模块中导入命名类型的语法如下所示:

import type { Type } from 'mod';

该语法中,Type 表示 mod 模块中导出的类型名。

从模块中导入所有导出的命名类型的语法如下所示:

import type * as TypeNS from 'mod';

该语法中,TypeNS 可以为任意标识符名,它将 mod 模块中的所有命名模块导出类型放在命名空间 TypeNS 下。

例如,有如下目录结构的工程:

C:\app
|-- index.ts
`-- utils.ts

utils.ts 文件的内容如下:

class Point {
    x: number;
    y: number;
}

export type { Point };

TypeScript 中的类既能表示一个值,又能表示一种类类型。此例中,我们只导出了 Point 类表示的类型。

index.ts 文件的内容如下:

import type { Point } from './utils';

const p: Point = { x: 0, y: 0 };

此例中,我们从 utils.ts 模块中导入了 Point 类型。需要注意的是,若将 Point 当作一个值来使用,则会产生编译错误。示例如下:

import type { Point } from './utils';

const p = new Point();
//            ~~~~~
//            编译错误:'Point' 不能作为值来使用,
//            因为它使用了'import type'导入语句

此外,就算在 index.ts 文件中不是使用 import type 来导入 Point,而是使用常规的 import 语句,也不能将 Point 作为一个值来使用,因为我们在 utils.ts 模块中只导出了 Point 类型,而没有导出 Point 值。示例如下:

import { Point } from './utils';

const p = new Point();
//            ~~~~~
//            编译错误:'Point' 不能作为值来使用,
//            因为它是由'export type'语句导出的

--importsNotUsedAsValues

针对类型的模块导入与导出的一个重要性质是,在编译生成 JavaScript 代码时,编译器一定会删除 import typeexport type 语句,因为能够完全确定它们只与类型相关。

例如,utils.ts 文件的内容如下:

class A {
    x: number = 0;
}
class B {
    x: number = 0;
}

export { A };
export type { B };

编译后生成的 utils.js 文件的内容如下:

class A {
    constructor() {
        this.x = 0;
    }
}
class B {
    constructor() {
        this.x = 0;
    }
}
export { A };

此例中,对类型 B 的导出语句被编译器删除了。

对于常规的 import 语句,编译器提供了 --importsNotUsedAsValues 编译选项来精确控制在编译时如何处理它们。该编译选项接受以下三个可能的值:

  • "remove"(默认值)。该选项是编译器的默认行为,它自动删除只和类型相关的 import 语句。

  • "preserve"。该选项会保留所有 import 语句。

  • "error"。该选项会保留所有 import 语句,发现可以改写为 import type 的 import 语句时会报错。

动态模块导入

动态模块导入允许在一定条件下按需加载模块,而不是在模块文件的起始位置一次性导入所有依赖的模块。因此,动态模块导入可能会提升一定的性能。动态模块导入通过调用特殊的 import() 函数来实现。该函数接受一个模块路径作为参数,并返回 Promise 对象。如果能够成功加载模块,那么 Promise 对象的完成值为模块对象。动态模块导入语句不必出现在模块的顶层代码中,它可以被用在任意位置,甚至可以在非模块中使用。

例如,有如下目录结构的工程:

C:\app
|-- index.ts
`-- utils.ts

utils.ts 文件的内容如下:

export function add(x: number, y: number) {
    return x + y;
}

index.ts 文件的内容如下:

setTimeout(() => {
    import('./utils')
        .then(utils => {
            console.log(utils.add(1, 2));
        })
        .catch(error => {
            console.log(error);
        });
}, 1000);

index.ts 中使用了 setTimeout 函数实现了在延迟 1 秒之后动态地导入 utils.ts 模块,然后调用了 utils.ts 模块中导出的 add 函数。

--module

TypeScript 编译器提供了 --module 编译选项来设置编译生成的 JavaScript 代码使用的模块格式。在 TypeScript 程序中,推荐使用标准的 ECMAScript 模块语法来进行编码,然后通过编译器来生成其他模块格式的代码。

该编译选项的可选值如下:

  • None(非模块代码)

  • CommonJS

  • AMD

  • System

  • UMD

  • ES6

  • ES2015

  • ES2020

  • ESNext

下面我们将配置编译器来生成符合 CommonJS 模块格式的代码。

例如,有如下目录结构的工程:

C:\app
|-- index.ts
`-- utils.ts

utils.ts 文件的内容如下:

export function add(x: number, y: number) {
    return x + y;
}

index.ts 文件的内容如下:

import { add } from './utils';

const total = add(1, 2);

C:\app 目录下运行 tsc 命令并指定 --module 编译选项来编译 index.ts,示例如下:

tsc index.ts --module CommonJS

关于 tsc 命令的详细介绍请参考 8.1 节。

运行 tsc 命令后,编译会分别生成 index.js 文件和 utils.js 文件,示例如下:

C:\app
|-- index.js
|-- index.ts
|-- utils.js
`-- utils.ts

编译生成的 utils.js 文件内容如下:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function add(x, y) {
    return x + y;
}
exports.add = add;

编译生成的 index.js 文件内容如下:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("./utils");
const total = utils_1.add(1, 2);

此例中,编译生成的 index.js 文件和 utils.js 文件都是符合 CommonJS 模块格式的代码。代码中的 __esModule 属性表示该文件是由 ECMAScript 模块代码编译而来的。如果想要生成符合其他模块格式的代码,只需要传入不同的 --module 值即可。