TypeScript: TypeScript 模块系统

最后更新:2026-08-26

模块是组织代码的基本单位——TypeScript 支持 ES 模块和 CommonJS 模块,并在此基础上增加了类型导出的能力。

1. ES 模块基础

(1) 命名导出

TYPESCRIPT
// utils.ts —— 命名导出
export function add(a: number, b: number): number {
  return a + b;
}

export function multiply(a: number, b: number): number {
  return a * b;
}

export const PI = 3.14159;
TYPESCRIPT
// main.ts —— 命名导入
import { add, multiply, PI } from "./utils";

console.log(add(1, 2));        // 3
console.log(multiply(3, 4));   // 12
console.log(PI);               // 3.14159

(2) 默认导出

TYPESCRIPT
// logger.ts —— 默认导出
export default class Logger {
  constructor(private prefix: string) {}

  log(message: string): void {
    console.log(`[${this.prefix}] ${message}`);
  }
}
TYPESCRIPT
// main.ts —— 默认导入(不需要花括号,名字自定义)
import Logger from "./logger";
// 也可以:import MyLogger from "./logger";

let logger = new Logger("APP");
logger.log("应用启动");  // [APP] 应用启动

(3) 同时使用默认导出和命名导出

TYPESCRIPT
// api.ts
export default class ApiClient {
  constructor(private baseUrl: string) {}

  async get(path: string): Promise<any> {
    // ...
  }
}

export enum HttpMethod {
  GET = "GET",
  POST = "POST",
  PUT = "PUT",
  DELETE = "DELETE"
}
TYPESCRIPT
// main.ts
import ApiClient, { HttpMethod } from "./api";

let client = new ApiClient("https://api.example.com");
let method: HttpMethod = HttpMethod.GET;

▶ 示例:模块化的计算器

TYPESCRIPT
// calculator/operations.ts
export function add(a: number, b: number): number { return a + b; }
export function subtract(a: number, b: number): number { return a - b; }
export function multiply(a: number, b: number): number { return a * b; }
export function divide(a: number, b: number): number {
  if (b === 0) throw new Error("除数不能为零");
  return a / b;
}
▶ 试一试
TYPESCRIPT
// calculator/index.ts
export { add, subtract, multiply, divide } from "./operations";
export type { Operation } from "./types";

// 默认导出——计算器类
import * as ops from "./operations";

export default class Calculator {
  compute(op: string, a: number, b: number): number {
    switch (op) {
      case "+": return ops.add(a, b);
      case "-": return ops.subtract(a, b);
      case "*": return ops.multiply(a, b);
      case "/": return ops.divide(a, b);
      default: throw new Error(`未知运算:${op}`);
    }
  }
}

2. 类型导出

TypeScript 可以单独导出类型——这是 JS 模块系统没有的能力:

(1) type 修饰符

TYPESCRIPT
// types.ts
export interface User {
  id: number;
  name: string;
  email: string;
}

export type UserId = number;

export type UserRole = "admin" | "editor" | "viewer";

// 用 export type 明确标记"只导出类型"
export type { User as UserType };

(2) 导入时区分类型和值

TYPESCRIPT
// main.ts
import { type User, type UserRole, createUser } from "./types";
//        ↑ type 修饰符表示"只导入类型"——编译后会被擦除

// 等价的旧语法
// import { User, UserRole } from "./types";  // 可能导致运行时导入

// 推荐新语法——明确区分类型导入和值导入
import type { User, UserRole } from "./types";
import { createUser } from "./types";
💡 为什么要区分? import type 导入的类型在编译后会被完全擦除——不会产生运行时的 requireimport 调用。这对于只使用类型的场景(如类型注解、接口)非常重要,避免不必要的模块加载。

(3) 内联 type 导入

TYPESCRIPT
// 混合导入——值和类型
import { createUser, type User, type UserRole } from "./types";

// createUser 是值——运行时需要
// User 和 UserRole 是类型——编译时擦除

3. 重新导出与桶文件

(1) 重新导出

TYPESCRIPT
// 从一个模块重新导出另一个模块的成员
export { User, UserId } from "./user-types";
export { Product, ProductId } from "./product-types";
export { Order, OrderId } from "./order-types";

(2) 桶文件(Barrel File)

index.ts 作为目录的入口,重新导出所有公共 API:

TYPESCRIPT
// models/index.ts —— 桶文件
export { User, UserId } from "./user";
export { Product, ProductId } from "./product";
export { Order, OrderId } from "./order";
export type { CreateUser, UpdateUser } from "./user";
export type { CreateProduct, UpdateProduct } from "./product";
TYPESCRIPT
// 使用时直接从目录导入
import { User, Product, type CreateUser } from "./models";
// 而不需要知道具体在哪个文件
💡 优点: 简化导入路径、隐藏内部文件结构、控制公共 API。缺点:可能导入不需要的模块(tree-shaking 可能无法完全优化)。


4. 模块解析策略

TypeScript 需要知道如何把 import "./utils" 解析为实际文件——这由模块解析策略决定。

(1) 两种解析策略

策略 用途 说明
classic 旧版兼容 先找 .ts,再找 .d.ts
node(推荐) 现代 TS 项目 模拟 Node.js 解析逻辑

(2) Node 解析策略的查找顺序

TEXT 📖 仅展示
import { X } from "./utils"

查找顺序:

  1. ./utils.ts
  2. ./utils.tsx
  3. ./utils.d.ts
  4. ./utils/package.json 中的 types 字段
  5. ./utils/index.ts
  6. ./utils/index.d.ts

(3) node_modules 查找

TEXT 📖 仅展示
import _ from "lodash"

查找顺序:

  1. ./node_modules/lodash.ts(不存在)
  2. ./node_modules/lodash/package.jsontypes/typings 字段
  3. ./node_modules/lodash/index.d.ts
  4. ./node_modules/@types/lodash/index.d.ts
  5. 向上查找 ../node_modules/../../node_modules/ ...

5. 路径映射(Path Mapping)

大型项目中用路径别名避免长长的相对路径:

(1) tsconfig.json 配置

JSON
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@utils/*": ["src/utils/*"],
      "@models/*": ["src/models/*"],
      "@components/*": ["src/components/*"]
    }
  }
}

(2) 使用路径别名

TYPESCRIPT
// 没有别名——相对路径容易出错
import { User } from "../../../models/user";
import { formatDate } from "../../utils/date";

// 有别名——清晰简洁
import { User } from "@models/user";
import { formatDate } from "@utils/date";
⚠️ 注意: 路径别名只是编译时的映射——运行时(Node.js/浏览器)不理解 @models 这样的路径。需要配合构建工具(Webpack resolve.alias、Vite resolve.alias、tsc-alias 等)做运行时路径替换。


6. CommonJS 互操作

(1) CommonJS 模块

TYPESCRIPT
// 用 CommonJS 风格导出
// math.cjs
const add = (a, b) => a + b;
const multiply = (a, b) => a * b;
module.exports = { add, multiply };

(2) 在 TS 中导入 CommonJS

TYPESCRIPT
// esModuleInterop: false(默认)
import * as math from "./math.cjs";
math.add(1, 2);

// esModuleInterop: true(推荐)
import math from "./math.cjs";   // ✅ 更自然的导入方式
math.add(1, 2);

(3) 允许从 TS 导出 CommonJS

TYPESCRIPT
// 用 export = 语法导出 CommonJS 风格
class Calculator {
  add(a: number, b: number): number { return a + b; }
}
export = Calculator;

// 导入时用 import = require
import Calculator = require("./calculator");
let calc = new Calculator();
💡 建议: 新项目统一用 ES 模块(import/export),开启 esModuleInterop: true 兼容旧的 CommonJS 包。export =import = 只在需要严格兼容 CommonJS 时使用。


❓ 常见问题

Q import type 和普通 import 有什么区别?
A import type 导入的类型在编译后会被完全擦除,不会产生运行时的模块加载。普通 import 既导入值也导入类型,编译后会有 require() 调用。只用类型时(如接口、类型别名),务必用 import type 避免无用的运行时导入。
Q 桶文件(index.ts)该不该用?
A 库和公共 API 推荐用——简化导入、控制导出接口。应用内部视情况而定——小项目可以不用,大项目有组织优势。主要缺点是可能影响 tree-shaking,但现代打包工具已经能很好地处理。
Q 路径别名在运行时怎么生效?
A TypeScript 编译后的 JS 仍然使用别名路径(如 @models/user),运行时不认识。需要配合构建工具做路径替换——Webpack 用 resolve.alias,Vite 用 resolve.alias,纯 tsc 编译需要 tsc-alias 等工具后处理。
Q ES 模块和 CommonJS 该用哪个?
A 新项目统一用 ES 模块(import/export)。CommonJS 是 Node.js 的旧模块系统,正在被淘汰。TypeScript 的 esModuleInterop 选项让你可以无缝使用 CommonJS 包。浏览器和 Deno 只支持 ES 模块。

📖 小节

📝 作业

  1. 基础题(难度⭐):创建三个模块文件——math.ts(导出 add/subtract)、string-utils.ts(导出 capitalize/reverse)、index.ts(桶文件重新导出)。在 main.ts 中导入并使用。
  2. 进阶题(难度⭐⭐):给已有项目配置路径映射——@utils 映射到 src/utils@models 映射到 src/models。用 import type 导入类型,普通 import 导入值。
  3. 挑战题(难度⭐⭐⭐):写一个声明文件让 CommonJS 包 legacy-sdk 在 TypeScript 中以 ES 模块风格导入——import LegacySDK from "legacy-sdk"。考虑 esModuleInterop 开启和关闭两种情况。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏