← Back to Index

Chapter 2.6: Modules

The modern way to organize code

🆚 Comparison with Java/Python: Java uses package + import, Python uses module + import. TS/JS's module system evolved from CommonJS → AMD → ESM, with ESM being the modern standard. Unlike Java's strict file-class mapping, a TS file can export any number of values.

1. ESM Modules

ES Modules is the official JavaScript module standard and the recommended module approach for 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 Compatibility

Many libraries in the Node.js ecosystem still use CommonJS format. Understanding its syntax and compatibility configuration is important:

// 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 is the recommended default configuration. It makes TS generate helper code to bridge ESM and CJS differences, allowing you to use import syntax uniformly.

3. Named Exports vs Default Exports

Both export approaches have their use cases, but the community consensus is to prefer named exports:

✅ Named Exports (Recommended)

  • • IDE auto-completion and refactoring friendly
  • • More reliable tree-shaking
  • • Consistent import names, less confusion
  • • One module can export multiple values

⚠️ Default Exports

  • • Importers can name freely, prone to inconsistency
  • • Suitable when a module has only one main export
  • • React component files commonly use default exports
  • • Some frameworks conventionally use default exports

4. Module Resolution

// 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";

Note: paths only affects the TS compiler's type checking. Actual runtime path resolution requires bundler tools (like Vite, webpack) or Node.js's --conditions flag.

5. Declaration Files .d.ts

Declaration files provide type information for untyped JavaScript libraries and are a key infrastructure of the TS ecosystem:

// 安装社区维护的类型定义(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. Namespaces

Namespaces were TS's early code organization approach, now replaced by ESM modules, but still encountered in some scenarios:

// 命名空间(旧模式,不推荐新项目使用)
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);

Modern guideline: New projects should always use ESM modules, not namespaces. Namespaces mainly appear in legacy code and .d.ts declaration files.

7. Project Module Organization

# 推荐的项目结构——按功能组织
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";

Avoid circular dependencies: Module A imports B, and B imports A will cause runtime errors. Use dependency injection, event bus, or extract shared modules to break cycles.

📝 Chapter Summary