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 导入的类型在编译后会被完全擦除——不会产生运行时的 require 或 import 调用。这对于只使用类型的场景(如类型注解、接口)非常重要,避免不必要的模块加载。
(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"
查找顺序:
./utils.ts./utils.tsx./utils.d.ts./utils/package.json中的types字段./utils/index.ts./utils/index.d.ts
(3) node_modules 查找
TEXT
📖 仅展示
import _ from "lodash"
查找顺序:
./node_modules/lodash.ts(不存在)./node_modules/lodash/package.json→types/typings字段./node_modules/lodash/index.d.ts./node_modules/@types/lodash/index.d.ts- 向上查找
../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 模块。📖 小节
- ES 模块用
export/import组织代码,命名导出用花括号,默认导出不用 import type只导入类型——编译后擦除,不产生运行时模块加载- 桶文件
index.ts重新导出目录内的公共 API,简化导入路径 - 模块解析策略
node(推荐)模拟 Node.js 查找逻辑 - 路径映射(
baseUrl+paths)用别名替代长相对路径,需构建工具配合 esModuleInterop: true让 ES 模块和 CommonJS 无缝互操作
📝 作业
- 基础题(难度⭐):创建三个模块文件——
math.ts(导出 add/subtract)、string-utils.ts(导出 capitalize/reverse)、index.ts(桶文件重新导出)。在main.ts中导入并使用。 - 进阶题(难度⭐⭐):给已有项目配置路径映射——
@utils映射到src/utils,@models映射到src/models。用import type导入类型,普通import导入值。 - 挑战题(难度⭐⭐⭐):写一个声明文件让 CommonJS 包
legacy-sdk在 TypeScript 中以 ES 模块风格导入——import LegacySDK from "legacy-sdk"。考虑esModuleInterop开启和关闭两种情况。