CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
模块与 tsconfig
这一课你将能做到
用 ES Module import / export 组织代码,区分类型导入和值导入
配置一份适合现代项目的 tsconfig.json,说清每个关键字段的作用
配置路径映射 @/ 别名,并在构建工具里同步生效
为什么要学
模块系统是大型项目的「文件管理系统」。没有模块,所有代码写在一个文件里,改一行就要理解全部。有了模块,每个文件只关心自己的输入(import)和输出(export),职责清晰、依赖透明。
而 tsconfig.json 是 TypeScript 项目的「总控台」:编译目标、模块规范、严格程度、路径别名,全在这里定。项目里 strict: true 到底开了什么?paths 别名为什么 @/ 指向 src?答案都在这一课。
先建立图像
模块化的核心概念:
┌──────────────┐ ┌──────────────┐
│ user.ts │ export │ app.ts │
│ 导出:User │────────▶│ 导入:User │
│ 导出:format │ import │ 使用:User │
└──────────────┘ └──────────────┘
类型和值都可以被导出/导入
编译时 TS 检查类型,构建时模块被打包成浏览器/Node 能执行的格式
tsconfig.json 的决策路径:
你的 .ts 源码
│
├── target: ES2020? ES2015? ES5? (编译成什么版本的 JS)
├── module: ESNext? CommonJS? (用什么模块规范)
├── strict: true? false? (严格程度开关)
├── paths: @/* → src/*? (路径别名)
└── outDir: dist/? (产物目录)
跟着做
新建项目文件夹,逐步实操。
步骤 1:创建项目结构
mkdir module-demo
cd module-demo
mkdir src
创建 src/types.ts:
export interface User {
id: number;
name: string;
email: string;
}
export type Role = "admin" | "editor" | "viewer";
创建 src/user.ts:
import type { User } from "./types";
export function createUser(name: string, email: string): User {
return { id: Date.now(), name, email };
}
export function formatUser(user: User): string {
return `#${user.id} ${user.name} <${user.email}>`;
}
创建 src/app.ts:
import { createUser, formatUser } from "./user";
const user = createUser("小明", "ming@example.com");
console.log(formatUser(user)); // #... 小明 <ming@example.com>
编译运行:
tsc --init
tsc
node dist/app.js
步骤 2:导出方式对比
// 方式一:具名导出(named export)——推荐
export interface User { id: number }
export function format() {}
// 导入时要写同名(可起别名)
import { User, format as fmt } from "./user";
// 方式二:默认导出(default export)——一个文件最多一个
export default function main() {}
// 导入时任意起名
import main from "./main";
规则 :一个模块最好用一种风格。React 组件常用 export default,工具函数常用 export function。混用会让 IDE 自动导入混乱。
步骤 3:类型导入与值导入 // 类型导入:编译后消失,运行时不存在
import type { User } from "./types";
// 值导入:运行时真的加载这个模块
import { createUser } from "./user";
// 混合:type 关键字内联标注
import { createUser, type User } from "./user";
为什么区分?TS 编译成 JS 时,类型导入会被完全删除;值导入会保留为 require 或 import 语句。如果工具类型只被用作类型,不写 type 会导致运行时加载一个实际上不存在的文件,构建工具可能报错。
步骤 4:tsconfig.json 核心配置详解 {
"compilerOptions": {
// 编译目标:产出什么版本的 JS
"target": "ES2020",
// 模块规范:ESNext(现代浏览器/打包工具)或 CommonJS(Node)
"module": "ESNext",
// 严格模式总开关(推荐 true)
"strict": true,
// 模块解析策略
"moduleResolution": "bundler",
// 输出目录
"outDir": "./dist",
// 源码根目录
"rootDir": "./src",
// 允许导入 JSON 文件
"resolveJsonModule": true,
// 生成声明文件(发布库时必需)
"declaration": true,
// 路径映射(简化深路径导入)
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*"]
}
字段 作用 推荐值 target编译后的 JS 语法版本 "ES2020" 或 "ES2022"module模块系统规范 浏览器用 "ESNext",Node 用 "CommonJS" strict严格模式总开关 永远 true moduleResolution解析策略 现代项目用 "bundler" outDir编译产物目录 "./dist"rootDir源码根目录 "./src"paths路径别名 "@/*": ["./src/*"]
步骤 5:路径别名实战 // src/components/ui/Button.tsx
import { theme } from "../../styles/theme";
import { utils } from "../../../lib/utils";
import { theme } from "@/styles/theme";
import { utils } from "@/lib/utils";
注意 :paths 只影响 TS 的类型检查和编译,运行时/打包工具不认识 。需要在构建工具里同步配置:
Vite :vite.config.ts 里配 resolve.alias
Webpack :resolve.alias
Node :tsc 编译后路径不生效,需要 tsconfig-paths 或打包时处理
步骤 6:Node.js 项目的特殊配置 {
"compilerOptions": {
"module": "CommonJS",
"moduleResolution": "Node",
"target": "ES2022",
"strict": true,
"outDir": "./dist",
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true
}
}
module: "CommonJS":Node 的传统模块规范(require / module.exports)
esModuleInterop: true:让 import express from "express" 能正确编译成 require("express"),不加会报 express_1.default is not a function
步骤 7:声明文件与类型导入 {
"compilerOptions": {
"declaration": true,
"declarationDir": "./types",
"emitDeclarationOnly": true
}
}
生成的 types/user.d.ts 只保留类型信息,不含实现,供其他 TS 项目引用。
讲清楚要点
ESM 是未来,CJS 是过去 :浏览器和现代打包工具全面转向 ESM;Node 18+ 也原生支持 ESM。新项目用 ESM,老项目(CommonJS)逐步迁移。
import type 是零成本的 :它只参与类型检查,编译后完全消失。养成习惯:只引入类型的文件用 import type。
paths 是编译期概念 :TS 检查类型时认识 @/,但 Node/浏览器不认识。打包工具必须同步配置,否则运行时模块找不到。
strict 不是可选项 :它包含 strictNullChecks、noImplicitAny 等 8 个子开关,关掉其中任何一个都会漏掉一类线上 bug。
模块解析是构建工具的事 :tsc 只负责类型检查和编译成 JS,真正打包、tree-shaking、路径替换是 Webpack/Vite/Rollup 的工作。
声明文件是库的入口 :import 一个库时 TS 先找 .d.ts 文件,找不到就会报「找不到模块或其类型声明」。
你可能会懵的地方
import { type User } 和 import type { User } 有区别吗?
效果相同,前者是 TS 4.5+ 的内联写法,后者是整体声明。前者适合混合导入(值 + 类型),后者适合纯类型导入。
esModuleInterop 是干嘛的?
解决 ESM 和 CJS 互操作的问题。没有它,import express from "express" 编译成 CJS 时会变成 express_1.default(),但 express 包导出的是 module.exports = express,没有 .default,导致运行时报错。
paths 别名为什么打包后不生效?
paths 只在 TS 编译阶段起作用,tsc 输出的是相对路径。运行时/打包阶段需要构建工具(Webpack/Vite)单独配置 alias,或者使用 tsconfig-paths 让 Node 也认识。
一个文件能同时有 export default 和 export 吗?
能,但不推荐。混合导出会让自动导入和 IDE 提示变得混乱,一个模块最好只选一种风格。
module: "ESNext" 和 module: "ES2022" 有区别吗?
ESNext 跟随最新标准,ES2022 固定到 ES2022 特性。建议用 ESNext 或 ES2022,差别不大,构建工具会再处理一遍。
错法对照 错法 问题 正解 循环依赖 a.ts → b.ts → a.ts 运行时可能拿到未初始化的值 提取公共逻辑到第三个模块打破循环 import * as _ from "lodash" 全量导入打包体积爆炸 按需导入 import { debounce } from "lodash-es" 只配 TS 的 paths 不配打包工具 alias 运行时报模块找不到 Webpack/Vite 里同步配 alias 在 Node 项目用 module: "ESNext" 不加额外配置 tsc 编译出浏览器 ESM,Node 跑不了Node 项目用 module: "CommonJS" 以为 strict: false 是「宽松模式」 8 个严格检查全关,类型系统形同虚设 永远 strict: true,有报错改代码
卡点急救
报错 Cannot find module '@/utils' :打包工具 alias 没配。检查 vite.config.ts 或 webpack.config.js 的 resolve.alias。
tsc 编译后 dist 里没有 .d.ts 文件 :declaration 没开或配置没生效。检查 tsconfig.json 的 declaration 字段。
Node 运行报错 exports is not defined :编译成了 ESM 但 Node 按 CJS 加载。改 module: "CommonJS" 或把项目配成 ESM(package.json 加 "type": "module")。
类型导入在构建后报错 :构建工具不认识 import type。确保工具链支持 TS(Vite 内置、Webpack 配 babel/ts-loader)。
面试常问
ES Module 和 CommonJS 的核心区别?
ESM 是静态的(编译时确定依赖,支持 tree-shaking),CJS 是动态的(运行时加载)。ESM 的 import 是声明式的,CJS 的 require 是函数调用。现代前端全面转向 ESM。
import type 的意义是什么?
显式标记「只引入类型」。编译后完全消失,减少运行时依赖;让打包工具知道可以安全地 tree-shake 掉这个导入;避免「值被误删导致的运行时错误」。
paths 别名在运行时为什么失效?
paths 只在 TS 编译阶段生效,tsc 编译成 JS 时会转成相对路径。运行时环境(Node/浏览器)不认识 @/,需要构建工具在打包时做路径替换。
什么是声明文件(.d.ts)?
只包含类型定义、不含实现的文件。作用:让 TS 项目引用 JS 库时获得类型提示;发布库时供使用方获得类型支持。
自测三问
import { type User } 和 import type { User } 有什么细微差别?
为什么 paths 配置了 @/,Webpack 打包后运行时报 Cannot find module?
esModuleInterop: true 解决的具体是什么问题?如果不加,import express from "express" 编译后会发生什么?
小结一句话 ESM 模块系统管代码组织,tsconfig.json 管编译行为;import type 做零成本类型引用,paths 简化导入路径但需构建工具同步——模块和配置是大型 TS 项目的地基。
延伸