🆚 与 Java/Python 的对比:Java 用 package + import,Python 用 module + import。TS/JS 的模块系统经历了 CommonJS → AMD → ESM 的演进,ESM 是现代标准。不像 Java 有严格的文件-类对应关系,TS 一个文件可以导出任意数量的值。
1. ESM 模块
ES Modules 是 JavaScript 的官方模块标准,也是 TypeScript 推荐的模块方式:
// ===== math.ts =====
// 命名导出
export function add(a: number, b: number): number {
return a + b;
}
export const PI = 3.14159;
// 默认导出
export default class Calculator {
multiply(a: number, b: number): number { return a * b; }
}
// ===== app.ts =====
// 导入命名导出
import { add, PI } from "./math";
// 导入默认导出
import Calculator from "./math";
// 导入全部并命名空间
import * as MathLib from "./math";
MathLib.add(1, 2);
// 重命名导入
import { add as sum } from "./math";
// 仅导入类型(编译后完全消除)
import type { SomeType } from "./types";
// 重新导出(Re-exports)
export { add, PI } from "./math";
export { default as Calculator } from "./math";
export * from "./math"; // 导出全部命名导出
export * as math from "./math"; // 导出为命名空间
2. CommonJS 兼容
Node.js 生态中大量库仍使用 CommonJS 格式,了解其语法和兼容配置很重要:
// CommonJS 风格(在 .js 或旧项目中常见)
const fs = require("fs");
module.exports = { myFunc };
module.exports.default = MyClass;
// 在 TS 中导入 CJS 模块,需配置 esModuleInterop
// tsconfig.json:
// { "compilerOptions": { "esModuleInterop": true } }
// 开启后可以用 ESM 语法导入 CJS 模块
import fs from "fs"; // 等价于 const fs = require("fs")
import { readFile } from "fs"; // 也能解构
esModuleInterop: true 是推荐的默认配置。它让 TS 生成辅助代码来桥接 ESM 和 CJS 的差异,使你能统一使用 import 语法。
3. 命名导出 vs 默认导出
两种导出方式各有适用场景,但社区的主流建议是优先使用命名导出:
✅ 命名导出(推荐)
- • IDE 自动补全和重构友好
- • Tree-shaking 更可靠
- • 导入名称一致,减少混乱
- • 一个模块可导出多个值
⚠️ 默认导出
- • 导入者可随意命名,易不一致
- • 适合模块只有一个主要导出值
- • React 组件文件常用默认导出
- • 某些框架约定使用默认导出
4. 模块解析
// tsconfig.json 中的模块解析配置
{
"compilerOptions": {
// 模块解析策略
"moduleResolution": "bundler", // 推荐:兼容现代打包工具
// "moduleResolution": "node", // 传统 Node.js 解析
// 路径别名——避免深层相对路径
"baseUrl": "./src",
"paths": {
"@/*": ["./*"],
"@utils/*": ["./utils/*"],
"@components/*": ["./components/*"]
}
}
}
// 使用路径别名后
import { formatDate } from "@utils/date"; // 代替 "../../../utils/date"
import { Button } from "@components/Button";
注意:paths 只影响 TS 编译器的类型检查。实际运行时的路径解析需要打包工具(如 Vite、webpack)或 Node.js 的 --conditions 配合。
5. 声明文件 .d.ts
声明文件为无类型的 JavaScript 库提供类型信息,是 TS 生态的关键基础设施:
// 安装社区维护的类型定义(DefinitelyTyped)
// npm install --save-dev @types/lodash @types/express
// 为无类型的 JS 库手写声明文件
// ===== types/legacy-lib.d.ts =====
declare module "legacy-lib" {
export function doSomething(input: string): number;
export interface Config {
verbose: boolean;
timeout: number;
}
export default function init(config: Config): void;
}
// 为全局变量添加类型(如 CDN 引入的库)
declare const jQuery: (selector: string) => any;
// 扩展已有模块的类型
declare module "express" {
interface Request {
userId?: string;
}
}
6. 命名空间
命名空间是 TS 早期的代码组织方式,现已被 ESM 模块取代,但在某些场景仍会遇到:
// 命名空间(旧模式,不推荐新项目使用)
namespace Validation {
export interface StringValidator {
isValid(s: string): boolean;
}
export class EmailValidator implements StringValidator {
isValid(s: string): boolean {
return s.includes("@");
}
}
}
const validator = new Validation.EmailValidator();
// 声明合并时仍可能用到命名空间
// 为已有类添加静态成员
class MyClass { }
namespace MyClass {
export const version = "1.0";
}
console.log(MyClass.version);
现代准则:新项目应始终使用 ESM 模块,不要使用命名空间。命名空间主要出现在旧代码和 .d.ts 声明文件中。
7. 项目模块组织
# 推荐的项目结构——按功能组织
src/
├── features/
│ ├── user/
│ │ ├── index.ts # Barrel file: 统一导出
│ │ ├── user.service.ts
│ │ ├── user.types.ts
│ │ └── user.controller.ts
│ └── order/
│ ├── index.ts
│ ├── order.service.ts
│ └── order.types.ts
├── shared/
│ ├── utils/
│ └── types/
└── index.ts # 应用入口
// ===== features/user/index.ts(Barrel file)=====
export { UserService } from "./user.service";
export { UserController } from "./user.controller";
export type { User, CreateUserDTO } from "./user.types";
// 使用时只需从 feature 目录导入
import { UserService, User } from "@/features/user";
避免循环依赖:模块 A 导入 B,B 又导入 A 会导致运行时错误。可用依赖注入、事件总线或提取共享模块来打破循环。
📝 本章要点
- ✦ESM(
import/export)是现代标准,优先使用命名导出 - ✦开启
esModuleInterop以兼容 CommonJS 模块 - ✦用
paths配置路径别名避免深层相对路径 - ✦
.d.ts声明文件和@types包为 JS 生态提供类型支持 - ✦按功能组织模块,用 Barrel file 统一导出,避免循环依赖