← 返回目录

第 2.6 章:模块

组织代码的现代方式

🆚 与 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 会导致运行时错误。可用依赖注入、事件总线或提取共享模块来打破循环。

📝 本章要点