TypeScript 模块
任何包含 import
或 export
语句的文件(.ts
、.d.ts
),视为一个 TS 模块(module
),对应的不包含 import、export 语句的文件会当作一个全局脚本文件。
TS 模块本身是一个作用域,不属于全局作用域。模块内部的变量、函数、类必须用 export 命令声明并在其他模块用 import 命令导入才能使用。
如果一个文件不包含 export 语句,但希望它作为一个模块(即内部变量对外不可见),可以在文件中添加一行空的导出语句:export {};
。
TS 模块除了支持所有 ES 模块的语法,还允许导入和导出类型。导入、导出类型也可以与 JS 导入、导出语法一致,但为了区分类型与变量,TS 引入了在类型或语句前添加 type
关键字的方案:
import type { A } from "./a";
// 或者 import { type A } from "./a";
// 或者 import { A } from "./a";
export interface B {}
type C = "c";
class D {}
export type { C, D };
// 或者 export { type C, type D };
2
3
4
5
6
7
8
9
10
11
导入的文件没有后缀名,因为 TS 允许加载模块时省略后缀名(自动定位 ts 文件)。使用 import type
或 export type
语句时,表示后续都是类型;而在类型前添加 type
关键字,表示后面的是一个类型。import type
语句也支持 JS 导入的所有规则,例如默认导入、全部导入:
import type DefaultType from "./a";
import type * as TypeNameSpace from "./b";
2
如果仅用 export type
导出了一个类(class
,例如上面的 D
),而没有用普通的导出。那么文件导出的只是一个类型,不能在其他模块中作为类本身导入并使用。
importsNotUsedAsValues
TS 特有的输入类型(type)的 import 语句,编译成 JavaScript 时怎么处理呢?TS 提供了 importsNotUsedAsValues
编译设置项,有三个可能的值:
remove
:这是默认值,自动删除导入类型的 import 语句。preserve
:保留导入类型的 import 语句。error
:保留导入类型的 import 语句(与 preserve 相同),但是必须写成 import type 的形式,否则报错。
例如:import { TypeA } from "./a";
。TypeA 是一个类型,此时:
remove
编译结果会将该语句删掉;preserve
编译结果会保留该语句,但删除掉涉及类型的部分;
编译后会变为:import "./a";
。导入语句不再导入任何值或类型,但是语句还是会执行,所以 a
文件中的代码还是会执行。
error
编译结果与preserve
相同,但编译过程中会报错,因为它要求导入类型必须添加type
关键字。所以语句必须写成import type { TypeA } from "./a";
或import { type TypeA } from "./a";
才不会报错。
模块定位
模块定位(module resolution)指的是一种算法,用来确定 import 语句和 export 语句里面的模块文件位置。TS 的编译参数 moduleResolution
就是用来指定定位算法的,常用算法有两种:
Classic
module 设为 es2015、 esnext、amd, system, umd 等等时的默认值
Classic 方法会议当前文件路径作为基准路径,计算相对模块位置,并在对应的路径中查找 .ts
、.tsx
(如果开启了 JSX 支持,后问也一样)、.d.ts
文件。
对于非相对模块,也是以当前文件路径作为起点,一层层查找上级目录中的 .ts
、.tsx
、.d.ts
文件
Node
module 设为 commonjs 时的默认值
Node 方法就是模拟 Node.js 的模块加载方法,也就是 require()
的实现方法。
相对模块依然是以当前脚本的路径作为“基准路径”。比如,脚本文件 a.ts
里面有一行代码let x = require("./b");
,TS 按照以下顺序查找:
- 当前目录是否包含
b.ts
、b.tsx
、b.d.ts
。如果不存在就执行下一步。 - 当前目录是否存在子目录
b
,该子目录里面的package.json
文件是否有types
字段指定了模块入口文件。如果不存在就执行下一步。 - 当前目录的子目录
b
是否包含index.ts
、index.tsx
、index.d.ts
。如果不存在就报错。
非相对模块则是以当前脚本的路径作为起点,逐级向上层目录查找是否存在子目录 node_modules
。比如,脚本文件 a.js
有一行 let x = require("b");
,TS 按照以下顺序进行查找:
- 当前目录的子目录
node_modules
是否包含b.ts
、b.tsx
、b.d.ts
。 - 当前目录的子目录
node_modules
,是否存在文件package.json
,该文件的types
字段是否指定了入口文件,如果是的就加载该文件。 - 当前目录的子目录
node_modules
里面,是否包含子目录@types
,在该目录中查找文件b.d.ts
。 - 当前目录的子目录
node_modules
里面,是否包含子目录 b,在该目录中查找index.ts
、index.tsx
、index.d.ts
。 - 进入上一层目录,重复上面 4 步,直到找到为止。
路径映射
TS 允许在 tsconfig.json
中手动指定模块的路径:
- baseUrl
baseUrl 可以手动指定模块的基准目录(表示 tsconfig 文件所在的目录,文件内的相对路径会以 baseUrl 为基准)
{
"compilerOptions": {
"baseUrl": "." // ”.“ 表示当前目录
}
}
2
3
4
5
- paths
paths 字段指定将模块导入映射到其他相对于 baseUrl 的路径。例如引入了一个没有类型声明的库A
,我们手动为 A 编写了类型文件src/types/a.d.ts
就可以通过 paths 映射在导入 A 时正确加载类型声明。paths 还支持使用匹配语法指定一系列文件与类型文件的映射关系:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"A": ["src/types/a"],
"app/*": ["./src/app/*"]
}
}
}
2
3
4
5
6
7
8
9
TIP
注意,上例的路径值是一个数组,可以指定多个路径。如果第一个脚本路径不存在,那么就加载第二个路径,以此类推。
- rootDirs
rootDirs 字段指定多个目录作为“根目录”,这样导入模块时会从多个目录中去查找。例如指定模块定位时,需要查找的不同的国际化目录:
{
"compilerOptions": {
"rootDirs": ["src/zh", "src/de"]
}
}
2
3
4
5
此时使用 import { A } from 'x';
时,会分别尝试从 根目录、src/zh、src/de 导入
TIP
由于模块定位的过程很复杂,tsc
命令有一个 --traceResolution
参数,能够在编译时在命令行显示模块定位的每一步。