大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
模块与 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";
配置 paths 后:
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 项目的特殊配置
如果写 Node 后端而不是前端:
{
"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:声明文件与类型导入
发布一个库时,要生成 .d.ts 声明文件:
{
"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 项目的地基。
延伸
- Handbook: Modules
- tsconfig 官方参考
- 下一课:React 组件 typing——props、state、事件、ref、children、泛型组件的完整类型写法。