TS模块系统详解
# 07.TS模块系统详解
ES Module 如何在 TS 中实现类型安全?import type、声明文件、模块增强——构建可扩展的模块化代码。
# 1. 案例引入
# 1.1 一个模块化的灾难
// user.ts —— 导出了太多东西
export class User {}
export function createUser() {}
export function updateUser() {}
export function deleteUser() {}
export const API_URL = "/api/users";
// ... 20 行后还在 export
// 另一文件
import { User, createUser } from "./user"; // 从哪里找?找不到定义在哪个文件
// 循环依赖——A 引用 B,B 引用 A
// import { something } from "./circle-b";
// 编译时可能不报错,运行时可能 undefined
# 1.2 好的模块化
// user/models.ts —— 只用 export type 暴露类型
export interface User { id: number; name: string; }
// user/api.ts —— 清晰的导出
import type { User } from "./models"; // 只导入类型,编译后完全消失
export async function getUser(id: number): Promise<User> { /* ... */ }
// 消费者
import { getUser } from "./user/api";
const user = await getUser(1); // user: User —— 类型自动传递
# 2. ES Module 导出与导入
# 2.1 导出的四种方式
// 1. 声明时导出
export const API_URL = "https://api.example.com";
export function fetchData() {}
export interface User { id: number; }
// 2. 默认导出(一个模块只能一个)
export default class App {}
// 3. 统一导出
const a = 1, b = 2;
export { a, b };
// 4. 重命名导出
export { a as valueA, b as valueB };
export { default as AppComponent } from "./App";
# 2.2 导入的六种语法
// 1. 命名导入
import { fetchData, API_URL } from "./api";
// 2. 默认导入
import App from "./App";
// 3. 混合导入
import App, { fetchData } from "./api";
// 4. 全部导入为命名空间
import * as Api from "./api";
Api.fetchData();
Api.API_URL;
// 5. 仅执行模块副作用(不导入任何绑定)
import "./init.css";
// 6. 重命名导入
import { fetchData as getData } from "./api";
# 3. import type / export type
# 3.1 为什么需要"只导入类型"
// 普通 import——编译后可能在 JS 中保留引用
import { User } from "./models"; // 编译后:var models_1 = require("./models");
// import type——编译后完全消失
import type { User } from "./models"; // 编译后:无任何代码
// 好处 1:避免循环依赖(类型不产生运行时引用)
// 好处 2:减小打包体积
// 好处 3:明确意图——"我只用类型"
# 3.2 混合导入
// 同时导入值和类型
import { createUser, type User } from "./user";
// export type——只导出类型
export type { User, Config } from "./types";
# 4. 模块解析策略
# 4.1 两种策略:node 与 classic
// tsconfig.json
{
"compilerOptions": {
"moduleResolution": "node" // 或 "classic" / "bundler"(TS 5.0+)
}
}
node 策略(推荐):
import { foo } from "./utils"
→ 找 ./utils.ts → ./utils/index.ts → package.json types
↓ 模仿 Node.js 的 require 解析
bundler 策略(TS 5.0+,配合 Vite/Webpack):
不需要写完整扩展名,支持 package.json exports
# 4.2 路径别名配置
// tsconfig.json
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@/*": ["./*"],
"@components/*": ["./components/*"],
"@utils/*": ["./utils/*"],
"@types/*": ["./types/*"]
}
}
}
// 优雅的导入
import { Button } from "@components/Button";
import { formatDate } from "@utils/date";
import type { User } from "@types/user";
# 4.3 路径别名与构建工具的联动
TS 只负责类型检查——不负责打包
必须让构建工具(Webpack/Vite)也识别路径别名
Vite (vite.config.ts):
resolve: { alias: { "@": "/src" } }
Webpack (webpack.config.js):
resolve: { alias: { "@": path.resolve(__dirname, "src") } }
# 5. namespace 命名空间
# 5.1 namespace 基础
// 命名空间——将相关代码组织在一个名字下
namespace Geometry {
export interface Point { x: number; y: number; }
export function distance(a: Point, b: Point): number {
return Math.sqrt((b.x - a.x) ** 2 + (b.y - a.y) ** 2);
}
}
const p1: Geometry.Point = { x: 0, y: 0 };
const d = Geometry.distance(p1, { x: 3, y: 4 });
# 5.2 namespace 与 ES Module 的选择
namespace —— 旧时代遗留(AMD/全局脚本时代)
优点:可以跨文件合并声明
缺点:现代构建工具不友好
ES Module —— 现代标准
优点:原生支持、树摇(Tree Shaking)、异步加载
缺点:不能同名合并
选型结论:
新项目 → ES Module(永远)
旧项目 → namespace → 逐步迁移为 ES Module
# 6. declare 全局声明
# 6.1 declare 的三类使用
// 1. 声明全局变量(给以 <script> 引入的库补类型)
declare var jQuery: (selector: string) => any;
// 2. 声明全局函数
declare function greet(name: string): void;
// 3. 声明全局模块
declare module "*.css" {
const content: Record<string, string>;
export default content;
}
# 6.2 模块扩充(Module Augmentation)
// 原始定义(node_modules 的第三方库)
// export interface AxiosInstance { get<T>(url: string): Promise<T>; }
// 你的项目——扩展 Axios 实例
import axios from "axios";
declare module "axios" {
interface AxiosInstance {
getUser<T>(): Promise<T>;
postForm<T>(data: FormData): Promise<T>;
}
}
// 现在可以使用扩展方法(类型安全)
const user = await axios.getUser<User>();
# 6.3 全局类型扩充
// 为 Window 对象添加自定义属性
declare global {
interface Window {
__APP_CONFIG__: {
apiUrl: string;
version: string;
};
}
}
// 使用
const apiUrl = window.__APP_CONFIG__.apiUrl; // string
# 7. 三斜线指令
// 引入类型声明文件(现代项目中极少使用——被 tsconfig types 替代)
/// <reference path="./other.d.ts" />
/// <reference types="node" /> // 引入 @types/node
/// <reference lib="dom" /> // 引入标准库
# 8. 模块化的最佳实践
# 8.1 导出设计原则
// ❌ 过度导出
// 每个函数、每个类型都 export —— 边界模糊
// ✅ 分层导出策略
// 1. 内部实现(不导出)
const INTERNAL_CONST = 42;
// 2. 公共 API(选择性导出)
export function getUser(id: number): Promise<User> { /* ... */ }
// 3. 类型入口(用 barrel export)
// index.ts
export type { User, Config } from "./types";
export { getUser, createUser } from "./user";
export { formatDate, formatCurrency } from "./utils";
# 8.2 循环依赖解决方案
// 问题:A 导入 B,B 导入 A
// ├── parent.ts: import { Child } from "./child"
// └── child.ts: import { Parent } from "./parent"
// 解决方案 1:提取共享类型到单独文件
// types.ts: export interface Parent { ... } export interface Child { ... }
// 解决方案 2:使用 import type(最推荐)
// child.ts:
import type { Parent } from "./parent"; // 类型导入不会产生循环依赖
# 9. 速查表
| 语法 | 作用 | 示例 |
|---|---|---|
export const | 导出值 | export const API = "..." |
export default | 默认导出 | export default class |
import type | 仅导入类型(编译后消失) | import type { User } |
export type | 仅导出类型 | export type { Config } |
import * as | 命名空间导入 | import * as API |
paths | tsconfig 路径别名 | "@/*": ["./src/*"] |
namespace | 旧式命名空间 | namespace X { export ... } |
declare var | 声明全局变量类型 | declare var jQuery |
declare module | 扩充模块类型 | declare module "*.css" |
declare global | 扩充全局类型 | declare global { interface Window } |
下一篇:08.TS工程配置实践
上次更新: 2026/06/24, 12:59:24