tsconfig 的魔鬼——module 和 moduleResolution

杰克-逊の黑豹,恰饭了啦 []( ̄▽ ̄)

tsconfig 的魔鬼------module 和 moduleResolution

起因

自己写着玩儿也好,还是工作中建新项目也好,基本都是使用脚手架 (scaffolding)创建,里面的 tsconfig.json,我们多数情况下只需要调整/添加部分配置选项。

我尝试过 vite create-react-app @nest/cli,看了一些 vscode 插件源码,回顾它们,我发现它们在配置 modulemoduleResolution不太相同。

这勾起了我的极大兴趣,到底这两个配置项有什么门道儿,官网解释的那点意思够不够用?

正好,我想尝试用 ts 开发 nodejs 程序,而且是 esModule 和 commonJS 两种风格的 nodejs 程序,顺手就可以研究研究这两个配置项。

只说结论

module

表示编译后得到的 js 代码,采取怎样的模块管理方式。

常见的值 CommonJS ESNext ES2020 NodeNext

CommonJS不用多说,生成的代码,用的就是 require module.exports的模块管理方式;

ESNext ES2020这种,生成的代码,用的就是import export的模块管理方式;

NodeNext比较特殊,要看 package.jsontype字段,这个字段如果是module的话,生成的代码,用的就是 import export

值得注意的是,module负责的是模块管理方式,不负责 js 语法版本,因为这是由target字段管理的;

选择哪个值

如果你开发的是 commonJS 风格的旧版本 nodejs 代码,就选择 CommonJS;

如果你开发的是 esModule 风格的新版本 nodejs 代码,就选择 ESNext or NodeNext;

如果你开发的是 esModule 风格的浏览器端运行的代码,就选择 ESNext

ESNext 和 ES2020 有什么区别

ESNext 总是表示最新版本的 esModule;

ES2020 这种,表示的是特定版本的 esModule;

一般来讲,向最新版本看齐即可,当然有明确的版本要求下,也可设置为特定版本的值。

moduleResolution

module字段理解起来很简单,但moduleResolution理解起来就比较麻烦了,因为它表达的具体是什么意思,除了看它的字段值,还要结合module字段值。

这个字段表示,ts 按照什么样的规则找到模块,将模块信息提供给你,让你在编写代码的时候,可以看到类型提示等等。

比如:

ts 复制代码
import { Jack } from "./util/name";

应该怎么去找到 ./util/name表示的模块呢?怎么拿到 Jack 的类型信息呢?

这里只选出最常用的可选值解释。

classic

ts 复制代码
// /demo/A.ts
import { B } from "./B";

会依次寻找:

  • /demo/B.ts
  • /demo/B.d.ts
ts 复制代码
// /demo/Hello/A.ts
import { B } from "B";

会依次寻找:

  • /demo/Hello/B.ts

  • /demo/Hello/B.d.ts

  • /demo/B.ts

  • /demo/B.d.ts

  • /B.ts

  • /B.d.ts

当然,classic只是 tsc自身默认的模块寻找方式,但这个方式已经不常用了。

Node

tsc会仿照早期 nodejs 的方式寻找模块。

module字段必须要设置为CommonJS, 否则你编写代码的时候不会有什么问题,编译的时候也不会有什么问题,但是用 node 执行代码的时候,会报错。那是因为早期 nodejs 是不支持 esModule 风格的代码。

ts 复制代码
// /demo/Hello/A.ts
import { B } from "./B";

依次寻找:

  • /demo/Hello/B.ts

  • /demo/Hello/B.tsx

  • /demo/Hello/B.d.ts

  • /demo/Hello/B/package.json(访问 "types" 字段)

  • /demo/Hello/B/index.ts

  • /demo/Hello/B/index.tsx

  • /demo/Hello/B/index.d.ts

ts 复制代码
// /demo/A.ts
import { B } from "B";

依次寻找:

  • /demo/node_modules/B.ts

  • /demo/node_modules/B.tsx

  • /demo/node_modules/B.d.ts

  • /demo/node_modules/B/package.json(访问"types"字段)

  • /demo/node_modules/@types/B.d.ts

  • /demo/node_modules/B/index.ts

  • /demo/node_modules/B/index.tsx

  • /demo/node_modules/B/index.d.ts

  • /node_modules/B.ts

  • /node_modules/B.tsx

  • /node_modules/B.d.ts

  • /node_modules/B/package.json(访问"types"字段)

  • /node_modules/@types/B.d.ts

  • /node_modules/B/index.ts

  • /node_modules/B/index.tsx

  • /node_modules/B/index.d.ts

Node16 or NodeNext

tsc按照新版本 node 的方式寻找模块。

新 node 下,esModule 和 commonJS 都是支持的,在解释的时候,也会包括这两种类型。

module: CommonJS
ts 复制代码
// /demo/Hello/A.ts
import { B } from "./B";

和上面的 Node 一样, 依次寻找:

  • /demo/Hello/B.ts

  • /demo/Hello/B.tsx

  • /demo/Hello/B.d.ts

  • /demo/Hello/B/package.json(访问 "types" 字段)

  • /demo/Hello/B/index.ts

  • /demo/Hello/B/index.tsx

  • /demo/Hello/B/index.d.ts

ts 复制代码
// /demo/A.ts
import { B } from "B";

此时略有不同,会依次寻找:

  • /demo/node_modules/B.ts

  • /demo/node_modules/B.tsx

  • /demo/node_modules/B.d.ts

  • /demo/node_modules/B/package.json(优先访问"exports"字段,后访问"types"字段)

  • /demo/node_modules/@types/B.d.ts

  • /demo/node_modules/B/index.ts

  • /demo/node_modules/B/index.tsx

  • /demo/node_modules/B/index.d.ts

  • /node_modules/B.ts

  • /node_modules/B.tsx

  • /node_modules/B.d.ts

  • /node_modules/B/package.json(优先访问"exports"字段,后访问"types"字段)

  • /node_modules/@types/B.d.ts

  • /node_modules/B/index.ts

  • /node_modules/B/index.tsx

  • /node_modules/B/index.d.ts

Node16 or NodeNext 下,tsc 会识别 package.json 中的 exports 字段。

但仍要注意,如果这个包不在 node_modules 里,那么 exports字段不会被识别的。

module: ESNext
ts 复制代码
// /demo/Hello/A.ts
import { B } from "./B";

在 vscode 里,这样写会报错,错误信息会告诉你,要给"./B"补全文件后缀.js

ts 复制代码
// /demo/Hello/A.ts
import { B } from "./B.js";

这样写就没问题了,编译和运行时都能通过。

你肯定会问,文件明明是 /demo/Hello/B.ts, 哪里来的 B.js ?

OK, 放心好了,B.js 指的就是 B.ts。

写成 B.js 的话,依旧是访问 B.ts 中的信息,里面的函数、类型等提示,在 A.ts 仍然可以获取到。

你要知道,tsc 在编译的时候,会将 B.ts 文件输出为 B.js,而在编译过程中,import 引用的路径不会做任何修改。

A.ts 中写的是 "./B.js", 输出的 A.js 中写的还是 "./B.js"。

在编译之后,B.js 不就有了吗,不就能找到了吗?

ts 复制代码
// /demo/A.ts
import { B } from "B";

和 module: CommonJS 一样,会依次寻找:

  • /demo/node_modules/B.ts

  • /demo/node_modules/B.tsx

  • /demo/node_modules/B.d.ts

  • /demo/node_modules/B/package.json(优先访问"exports"字段,后访问"types"字段)

  • /demo/node_modules/@types/B.d.ts

  • /demo/node_modules/B/index.ts

  • /demo/node_modules/B/index.tsx

  • /demo/node_modules/B/index.d.ts

  • /node_modules/B.ts

  • /node_modules/B.tsx

  • /node_modules/B.d.ts

  • /node_modules/B/package.json(优先访问"exports"字段,后访问"types"字段)

  • /node_modules/@types/B.d.ts

  • /node_modules/B/index.ts

  • /node_modules/B/index.tsx

  • /node_modules/B/index.d.ts

Bundler

在 vscode 编写代码的时候,不会遇到什么错误,但是不能用 tsc 编译代码!

在搜索模块上,基本和 Node16 or NodeNext 一样。

唯一的区别在于 module: ESNext 情形下的相对引入:

ts 复制代码
// /demo/Hello/A.ts
import { B } from "./B";

不会要求你添加 .js 后缀,会依次寻找:

  • /demo/Hello/B.ts

  • /demo/Hello/B.tsx

  • /demo/Hello/B.d.ts

  • /demo/Hello/B/package.json(访问 "types" 字段)

  • /demo/Hello/B/index.ts

  • /demo/Hello/B/index.tsx

  • /demo/Hello/B/index.d.ts

那该怎么去编译代码呢?

Bundler 不都告诉你了嘛 ,你需要使用 bundler 处理,比如 rollup webpack 等工具。

怎么搭配 module 和 moduleResolution

直接用脚手架搞定的,可以直接跳过。

如果打算编写 commonJS 风格的 nodejs 程序,不支持解析exports字段:

module: "CommonJS"

moduleResolution: "Node"

如果打算编写 commonJS 风格的 nodejs 程序,支持exports字段:

module: "CommonJS"

moduleResolution: "NodeNext"

或者

module: "NodeNext"

moduleResolution: "NodeNext"

如果打算编写 esModule 风格的 nodejs 程序:

module: "ESNext"

moduleResolution: "NodeNext"

或者

module: "NodeNext"

moduleResolution: "NodeNext"

别忘了设置 package.json 的 type: "module"

如果打算编写浏览器端的代码:

module: "ESNext"

moduleResolution: "Bundler"

paths 陷阱

引入路径可能很长,我们会习惯使用 tsconfig.json 的 paths 配置项,简化路径。

但请注意,在编写代码的时候,vscode 会解析 paths,找到对应的模块,把模块的信息提供给你,然后你就能得到类型信息等智能提示了。但是在编译的时候,这些路径不会被转化成真实的路径!你需要用 bundler 工具去落实真正路径的转化。

如果你使用过 vite 的话,tsconfig.json 中的 paths 只是方便你写代码的时候获取模块信息,真正运行的时候,你还需要在 vite.config.json 做出 path 配置,才能编译通。

验证

非常简单,自己在本地搭建个项目,按照上述总结那般,做个探索即可。

这里给出一个简易参考。

lua 复制代码
project
  |------- tsconfig.json
  |------- index.ts
  |------- util
  |         |------ index.ts
  |
  |------- node_modules
                  |------ A
                          |------ package.json
                          |------ index.d.ts
                          |------ exports.d.ts
json 复制代码
// project/tsconfig.json

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "CommonJS",
    "moduleResolution": "Node"
  },
  "include": ["*.ts", "./util/*.ts"]
}
ts 复制代码
// project/index.ts

import { hello } from "./util";
import { say } from "A";

hello();
ts 复制代码
// project/util/index.ts

export function hello() {
  console.log("hello world");
}
json 复制代码
// project/node_modules/A/package.json

{
  "types": "./index.d.ts",
  "exports": {
    ".": {
      "types": "./exports.d.ts"
    }
  }
}
ts 复制代码
// project/node_modules/A/index.d.ts

export function say(): void;
ts 复制代码
// project/node_modules/A/exports.d.ts

export function joke(): void;

验证 exports 字段

将 tsconfig.json 的 moduleResolution 改为 Bundler,

index.ts 会报错,找不到函数 say;

验证相对路径引入时,不识别 exports 字段

将 tsconfig.json 的 moduleResolution 改为 Bundler,

将 A 从 node_modules 中复制到 project 下,在 index.ts 中加入

ts 复制代码
import { joke } "./A"

会发现 joke 找不到;

验证 .js 补全

将 tsconfig.json 的 moduleResolution 改为 NodeNext, module 改为 ESNext

index.ts 就会报错,改成 ./util/index.js 后错误消失,采用tsc编译,并用 node 执行编译结果入口文件,一切 OK

相关推荐
崔庆才丨静觅1 天前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60611 天前
完成前端时间处理的另一块版图
前端·github·web components
掘了1 天前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅1 天前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅1 天前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅1 天前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment1 天前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅1 天前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊1 天前
jwt介绍
前端
爱敲代码的小鱼1 天前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax