TypeScript 模块(详细教程)

什么是 TypeScript 模块?

在 JavaScript 项目中,随着代码量的增长,文件越来越多,逻辑越来越复杂,很容易陷入“全局变量污染”和“函数命名冲突”的困境。想象一下:你和同事同时写了一个叫 getUserInfo() 的函数,结果运行时谁覆盖了谁,调试起来非常头疼。

TypeScript 模块就是为了解决这个问题而生的。它把代码划分成独立的、可复用的单元,每个模块内部的变量和函数默认是私有的,不会自动暴露到全局作用域。这就像把一个大工厂拆分成多个车间,每个车间有自己的工具和工人,互不干扰,但又能通过标准接口协作。

在 TypeScript 中,模块是基于 ES6 的 importexport 语法实现的。它们不仅让代码更清晰,还能配合构建工具(如 Webpack、Vite)进行优化,比如代码分割和 tree-shaking(移除未使用的代码)。

💡 提示:TypeScript 模块是现代前端开发的基石,无论是开发库、框架,还是大型应用,几乎都依赖模块系统来组织代码。


模块的基本语法:export 与 import

TypeScript 模块的核心是 exportimport。你可以把它们理解为“打包”和“拆包”——把一组功能打包成一个模块,再在其他地方导入使用。

命名导出(Named Export)

命名导出允许你导出多个函数、变量或类。每个导出都有一个名字,导入时必须用相同的名字。

// mathUtils.ts
export const PI = 3.14159;

export function add(a: number, b: number): number {
  return a + b;
}

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

在另一个文件中,你可以这样导入:

// app.ts
import { PI, add, multiply } from './mathUtils';

console.log(PI); // 输出:3.14159
console.log(add(2, 3)); // 输出:5
console.log(multiply(4, 5)); // 输出:20

✅ 说明:export 关键字让这些内容可以被外部访问;import 从指定路径加载模块,花括号 {} 中的名字必须与导出的名字一致。


默认导出(Default Export)

默认导出只允许一个,通常用于模块的主要功能。导入时不需要花括号,名字可以自定义。

// logger.ts
const log = (message: string): void => {
  console.log(`[LOG] ${message}`);
};

export default log; // 只能有一个默认导出

导入时:

// main.ts
import log from './logger'; // 可以随便命名,比如 import logger from './logger'

log('程序启动成功'); // 输出:[LOG] 程序启动成功

⚠️ 注意:默认导出不能与命名导出共存于同一个文件中,除非你明确区分。但一个文件可以同时有默认导出和命名导出。


模块路径与文件结构

在 TypeScript 中,模块的路径是相对路径或绝对路径。推荐使用相对路径,清晰明了。

相对路径

// app.ts
import { add } from './utils/math'; // 当前目录下的 utils/math.ts
import { config } from '../config'; // 上一级目录的 config.ts

绝对路径(通过 tsconfig.json 配置)

你可以通过 tsconfig.json 设置别名,让导入更简洁。

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

然后就可以这样导入:

import { formatDate } from '@utils/date'; // 等价于 src/utils/date.ts
import { Button } from '@components/Button'; // 等价于 src/components/Button.ts

🧩 小贴士:别名能大幅提升项目结构的可读性,尤其在大型项目中非常实用。


模块的类型定义与类型导出

TypeScript 模块不仅能导出函数和变量,还能导出类型定义,让类型系统在模块之间保持一致。

// types.ts
export type User = {
  id: number;
  name: string;
  email: string;
};

export interface Post {
  title: string;
  content: string;
  author: User;
}

在其他文件中使用:

// blog.ts
import { User, Post } from './types';

const author: User = {
  id: 1,
  name: '张三',
  email: 'zhangsan@example.com'
};

const post: Post = {
  title: 'TypeScript 模块入门',
  content: '本文介绍模块的基本用法...',
  author
};

console.log(post.title); // 输出:TypeScript 模块入门

📌 重点:类型导出是模块化开发中非常关键的一环。它让团队成员之间能共享类型,避免“类型不一致”问题。


模块的命名空间 vs 模块(历史对比)

在早期的 TypeScript 版本中,有“命名空间”(namespace)这一概念,用于组织代码。但现在,模块已经取代了命名空间,成为主流方式。

特性 命名空间(namespace) 模块(module)
作用域 全局命名空间内 独立作用域
导出方式 export 关键字(在命名空间内) export 关键字(在模块顶层)
文件粒度 一个文件一个命名空间 一个文件一个模块
推荐使用 ❌ 已弃用 ✅ 强烈推荐

🔍 说明:虽然命名空间在 TypeScript 中仍可使用,但现代项目应统一使用模块。模块是 ES6 的标准,兼容性好,且支持 tree-shaking,更利于性能优化。


实战案例:构建一个小型工具库

我们来实战一个完整的 TypeScript 模块项目,模拟一个“工具库”模块,包含日期处理、字符串工具和配置管理。

项目结构

utils/
├── date.ts
├── string.ts
├── config.ts
└── index.ts

date.ts:日期处理模块

// date.ts
import { format } from 'date-fns'; // 日期格式化库(需 npm install date-fns)

// 格式化日期为 YYYY-MM-DD
export function formatDate(date: Date): string {
  return format(date, 'yyyy-MM-dd');
}

// 获取今天是星期几(中文)
export function getWeekday(date: Date): string {
  const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
  return `星期${weekdays[date.getDay()]}`;
}

string.ts:字符串工具

// string.ts
// 去除首尾空格并转为小写
export function cleanString(str: string): string {
  return str.trim().toLowerCase();
}

// 判断是否为邮箱格式
export function isValidEmail(email: string): boolean {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}

config.ts:配置管理

// config.ts
// 应用配置项
export const APP_CONFIG = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  debug: false
};

index.ts:统一导出入口

// index.ts
// 为外部提供统一的入口,隐藏内部结构

export { formatDate, getWeekday } from './date';
export { cleanString, isValidEmail } from './string';
export { APP_CONFIG } from './config';

使用模块库

// main.ts
import { formatDate, getWeekday, cleanString, isValidEmail, APP_CONFIG } from './utils';

console.log(formatDate(new Date())); // 输出:2024-04-05
console.log(getWeekday(new Date())); // 输出:星期五

console.log(cleanString('  Hello World  ')); // 输出:hello world
console.log(isValidEmail('test@example.com')); // 输出:true

console.log(APP_CONFIG.apiUrl); // 输出:https://api.example.com

✅ 这个案例展示了模块的三大优势:

  • 可维护性:功能按职责拆分
  • 可复用性:其他项目可直接引用 utils 模块
  • 可读性:通过 index.ts 统一入口,调用更简洁

常见问题与最佳实践

1. 模块导入路径错误?

常见错误:

import { add } from './math'; // 错误:文件名是 math.ts,但路径写成 math

✅ 正确做法:

import { add } from './math'; // 注意文件扩展名:.ts 可省略,但建议保留

2. 模块循环依赖?

当 A 模块导入 B,B 又导入 A,就会形成循环依赖。

// a.ts
import { bFunction } from './b'; // 导入 b
export function aFunction() {
  console.log('A');
  bFunction(); // 调用 b
}
// b.ts
import { aFunction } from './a'; // 导入 a
export function bFunction() {
  console.log('B');
  aFunction(); // 调用 a
}

❌ 这会导致运行时错误。

✅ 解决方案:重构代码,将共同逻辑提取到第三方模块,或延迟导入。

3. 最佳实践总结

  • 每个文件一个模块,职责单一
  • 使用 index.ts 统一导出,提升可维护性
  • 避免循环依赖,尽早拆分公共逻辑
  • 使用 typeinterface 做类型定义,增强类型安全
  • 配置 tsconfig.jsonmoduleResolutionnode16bundler,提升导入体验

结语

TypeScript 模块是构建现代 JavaScript 应用的基石。它不仅解决了代码组织混乱的问题,还为类型安全、代码复用和构建优化提供了强大支持。

无论你是初学者还是中级开发者,掌握模块的使用方式,都是迈向专业开发的第一步。从今天开始,把你的代码拆成模块,让项目更清晰、更健壮。

当你在项目中看到 importexport 时,别再觉得它们只是语法糖——它们是你代码结构的骨架,是团队协作的桥梁,更是未来可维护性的保证。