第 106 章:模块系统(Modules)
学习目标
- 掌握 ES Module 与 CommonJS 的区别
- 学会 TS 中的 import/export 语法
- 理解模块解析策略与路径映射
- 掌握类型导入导出与模块声明
一、什么是模块
模块是独立的作用域,每个文件就是一个模块。
typescript
// user.ts
const name = 'Tom';
export { name };
// app.ts
import { name } from './user';
console.log(name);二、模块系统对比
2.1 ES Module(推荐)
typescript
// 导出
export const a = 1;
export function foo() {}
export interface User { name: string }
// 默认导出
export default class UserService {}
// 导入
import UserService, { a, foo, User } from './module';2.2 CommonJS(Node.js)
typescript
// 导出
module.exports = { a: 1, foo: function() {} };
exports.a = 1;
// 导入
const { a, foo } = require('./module');2.3 对比
| 特性 | ES Module | CommonJS |
|---|---|---|
| 语法 | import / export | require / module.exports |
| 加载 | 异步、静态 | 同步、动态 |
| 顶层 await | ✅ 支持 | ❌ 不支持 |
| Tree-shaking | ✅ 友好 | ❌ 不友好 |
| 浏览器 | ✅ 原生 | ❌ 需打包 |
| Node.js | ✅ 14+ | ✅ 全部 |
三、export 的各种写法
3.1 命名导出
typescript
// 写法 1:逐个导出
export const a = 1;
export function foo() {}
export interface User { name: string }
// 写法 2:统一导出
const a = 1;
function foo() {}
interface User { name: string }
export { a, foo, User };
// 写法 3:重命名导出
const internal = 'hello';
export { internal as publicName };3.2 默认导出
typescript
// 一个模块只能有一个默认导出
export default class UserService {
// ...
}
// 等价于
class UserService { /* ... */ }
export default UserService;3.3 混合导出
typescript
// 同时有命名导出和默认导出
export const VERSION = '1.0.0';
export default class App {}3.4 Re-export(聚合)
typescript
// utils/index.ts 聚合多个文件
export * from './string';
export * from './number';
export { default as Date } from './date';四、import 的各种写法
4.1 基本导入
typescript
// 导入命名导出
import { a, foo } from './module';
// 导入默认导出
import UserService from './module';
// 导入全部
import * as Module from './module';
// 混合
import UserService, { a, foo } from './module';4.2 重命名导入
typescript
import { a as alias, foo as fn } from './module';4.3 副作用导入
typescript
// 只执行模块,不导入任何值
import './polyfill';4.4 动态导入
typescript
// 按需加载
async function loadModule() {
const module = await import('./heavy-module');
module.doSomething();
}
// 条件加载
if (condition) {
import('./module-a');
} else {
import('./module-b');
}五、类型导入导出
5.1 单独导入类型
typescript
// ✅ 用 import type
import type { User } from './types';
import { type User, fetchUser } from './api'; // 混合导入
// 优势:
// 1. 编译后被完全移除,不会进入运行时
// 2. 某些场景下必须用(isolatedModules)5.2 单独导出类型
typescript
// ✅ 用 export type
export type { User, Role };
export { type User, fetchUser }; // 混合导出5.3 实战:清晰的类型/值分离
typescript
// types.ts
export interface User {
id: number;
name: string;
}
export type Status = 'active' | 'inactive';
// api.ts
import type { User, Status } from './types';
export async function fetchUser(id: number): Promise<User> {
// ...
}
export async function updateStatus(id: number, status: Status): Promise<void> {
// ...
}六、模块解析
TS 解析模块路径时,按顺序尝试不同的扩展名和文件。
6.1 相对路径
typescript
import { foo } from './module'; // 当前目录
import { bar } from '../parent/module'; // 上级目录6.2 非相对路径
typescript
import React from 'react'; // node_modules
import { Button } from '@/components'; // 路径别名6.3 TS 解析策略
json
// tsconfig.json
{
"compilerOptions": {
"moduleResolution": "node" // 或 "node16" / "bundler" / "classic"
}
}| 策略 | 适用场景 |
|---|---|
node | 传统 Node.js 项目 |
node16 | Node.js 16+,支持 ESM |
bundler | Vite / Webpack 5+ 等现代打包器 |
classic | TS 0.6 时代,基本不用 |
七、路径别名(Path Mapping)
json
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"]
}
}
}typescript
// 使用
import { Button } from '@components/Button';
import { formatDate } from '@utils/date';提示
路径别名只是 TS 解析用,运行时需要打包器(Vite/Webpack)支持。
八、模块声明(Declaration)
8.1 声明第三方模块
typescript
// 没有类型定义的模块
declare module 'my-lib' {
export function foo(): void;
export const bar: number;
}8.2 声明全局模块
typescript
// 把模块扩展为全局
declare module '*.css' {
const content: { [className: string]: string };
export default content;
}
declare module '*.svg' {
const src: string;
export default src;
}
// 使用
import logo from './logo.svg'; // 类型: string8.3 声明全局变量
typescript
// 全局变量
declare const __APP_VERSION__: string;
declare const __DEV__: boolean;
// 使用
console.log(__APP_VERSION__); // 类型: string8.4 扩展已有模块
typescript
// 扩展 React 模块
declare module 'react' {
interface ComponentProps {
customProp?: string;
}
}九、CommonJS 互操作
9.1 导入 CommonJS 模块
typescript
// 第三方 CommonJS 模块
import * as fs from 'fs';
// 或
import fs from 'fs';
// 启用 esModuleInterop 后:
import fs from 'fs'; // 直接 default importjson
// tsconfig.json
{
"compilerOptions": {
"esModuleInterop": true, // 启用互操作
"allowSyntheticDefaultImports": true
}
}9.2 启用严格 ESM
json
// package.json
{
"type": "module" // 所有 .js 当作 ESM
}十、namespace(命名空间)
提示
namespace 是 TS 早期为内部模块设计的,现代项目优先用 ES Module。
typescript
// 旧式命名空间
namespace Utils {
export function formatDate(d: Date) { /* ... */ }
export class StringHelper { /* ... */ }
}
// 使用
Utils.formatDate(new Date());
// 嵌套
namespace Outer {
export namespace Inner {
export const foo = 1;
}
}
Outer.Inner.foo;namespace vs module:
| 特性 | namespace | module |
|---|---|---|
| 文件 | 通常单文件 | 一个文件即一个模块 |
| 现代项目 | ❌ 不推荐 | ✅ 推荐 |
| 适用场景 | 全局类型扩展 | 业务代码组织 |
十一、实战:项目模块组织
src/
├── api/ # API 请求
│ ├── user.ts
│ └── index.ts # 聚合导出
├── components/ # 公共组件
├── types/ # 全局类型
│ ├── user.ts
│ └── index.ts
├── utils/ # 工具函数
├── hooks/ # 自定义 hooks
└── main.ts # 入口typescript
// types/index.ts
export * from './user';
export * from './common';
// api/index.ts
export * from './user';typescript
// main.ts
import { User, fetchUser } from './api';
import type { Status } from './types';十二、tsconfig 模块相关配置
json
{
"compilerOptions": {
"module": "ESNext", // 输出模块格式
"moduleResolution": "Bundler", // 解析策略
"esModuleInterop": true, // 互操作
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true, // 允许导入 JSON
"isolatedModules": true, // 强制每个文件独立编译
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}十三、常见错误
13.1 类型被当成值
typescript
// ❌ 错误:把类型当值导入
import { User } from './types';
const u = new User(); // ❌ User 是类型,不是值
// ✅ 解决 1:用 import type
import type { User } from './types';
// ✅ 解决 2:类型可以这样用
const u: User = { id: 1, name: 'Tom' };13.2 循环依赖
typescript
// a.ts
import { b } from './b';
export const a = b + 1;
// b.ts
import { a } from './a'; // ❌ a 可能是 undefined
export const b = 1;
// 解决:重构代码,或者提取公共部分13.3 路径别名在测试中失效
typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import path from 'path';
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
}
});十四、本章小结
| 要点 | 关键 |
|---|---|
| 模块系统 | ES Module(推荐) / CommonJS |
| export | 命名、默认、Re-export |
| import | 命名、默认、重命名、动态、副作用 |
| 类型导入 | import type / export type |
| 路径解析 | moduleResolution + paths |
| 模块声明 | declare module |
| 互操作 | esModuleInterop: true |
| 现代项目 | 优先 ES Module,不用 namespace |
动手练习
- 聚合导出:创建一个
utils/index.ts,聚合 string / number / date 三个工具文件 - 路径别名:配置
tsconfig.json,让@/指向src/ - 类型导入:把项目中的类型导入都改成
import type - 模块声明:声明
*.svg和*.css模块的类型
推荐阅读
- 📖 TypeScript Handbook - Modules — 模块官方文档
- 📖 MDN - JavaScript Modules — ES Module 详解
- 🌐 TypeScript Module Resolution — 模块解析
下一章:第 107 章:命名空间(Namespace) →