命名空间与模块
TypeScript命名空间、ES模块、CommonJS模块、模块解析策略与声明文件详解。
1. TypeScript 模块系统概述
1.1 命名空间 vs 模块
| 特性 | 命名空间(Namespace) | 模块(Module) |
|---|---|---|
| 引入方式 | 全局可用 | 需要import |
| 文件级别 | 可跨文件合并 | 每个文件一个模块 |
| 推荐程度 | 旧代码/全局库 | 推荐 |
| 依赖管理 | 无(script标签顺序) | 打包工具处理 |
| 作用域 | 全局作用域 | 文件作用域 |
推荐:新项目使用ES模块,命名空间仅用于兼容旧代码或声明文件。
2. 命名空间
2.1 基本用法
// utils.ts
namespace Utils {
// 默认私有,export的才可外部访问
export function formatDate(date: Date): string {
return date.toISOString().split('T')[0];
}
export function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
// 未export,命名空间外部不可访问
function internalHelper(): void {
console.log('Internal');
}
// 嵌套命名空间
export namespace Math {
export function randomRange(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
}
}
// 使用
const today = Utils.formatDate(new Date());
const clamped = Utils.clamp(150, 0, 100);
const random = Utils.Math.randomRange(1, 10);
2.2 跨文件合并
// shapes.ts
namespace Shapes {
export class Circle {
constructor(public radius: number) {}
area(): number {
return Math.PI * this.radius ** 2;
}
}
}
// more-shapes.ts
// 同名命名空间会自动合并
namespace Shapes {
export class Rectangle {
constructor(
public width: number,
public height: number
) {}
area(): number {
return this.width * this.height;
}
}
}
// 使用时两个类都可用
const circle = new Shapes.Circle(5);
const rect = new Shapes.Rectangle(10, 20);
2.3 命名空间与类的合并
// 类 + 命名空间合并(类似静态方法)
class Album {
constructor(public name: string) {}
}
namespace Album {
// 类的静态工厂方法
export function fromJSON(json: string): Album {
const data = JSON.parse(json);
return new Album(data.name);
}
export const DEFAULT = new Album('Untitled');
}
const album = Album.fromJSON('{"name":"Greatest Hits"}');
const defaultAlbum = Album.DEFAULT;
3. ES 模块
3.1 导出
// === 具名导出 ===
// types.ts - 类型导出
export interface User {
id: number;
name: string;
email: string;
}
export type UserRole = 'admin' | 'editor' | 'viewer';
export enum Status {
Active = 'active',
Inactive = 'inactive',
}
// utils.ts - 函数导出
export function formatDate(date: Date): string {
return date.toISOString().split('T')[0];
}
export function debounce<T extends (...args: any[]) => any>(
fn: T,
delay: number
): (...args: Parameters<T>) => void {
let timer: ReturnType<typeof setTimeout>;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
// === 默认导出 ===
// 每个模块只能有一个默认导出
export default class UserService {
private users: Map<number, User> = new Map();
addUser(user: User): void {
this.users.set(user.id, user);
}
getUser(id: number): User | undefined {
return this.users.get(id);
}
}
// === 重新导出 ===
export { User, UserRole } from './types';
export { formatDate } from './utils';
export * from './constants'; // 重新导出所有
3.2 导入
// === 具名导入 ===
import { User, UserRole } from './types';
import { formatDate } from './utils';
// === 默认导入 ===
import UserService from './services/UserService';
// === 混合导入 ===
import UserService, { User } from './services/UserService';
// === 命名别名 ===
import { formatDate as fmtDate } from './utils';
// === 全部导入 ===
import * as Utils from './utils';
Utils.formatDate(new Date());
// === 类型导入(仅导入类型,运行时擦除) ===
import type { User, UserRole } from './types';
// TypeScript 3.8+ 内联type修饰符
import { type User, formatDate } from './types';
3.3 动态导入
// 动态导入:按需加载,返回Promise
async function loadChart(): Promise<void> {
const { Chart } = await import('./chart');
const chart = new Chart('#container');
chart.render();
}
// 条件加载
if (featureFlags.advancedEditor) {
const { AdvancedEditor } = await import('./AdvancedEditor');
new AdvancedEditor();
}
4. 模块解析
4.1 解析策略
// tsconfig.json
{
"compilerOptions": {
"moduleResolution": "bundler" // 推荐
// "node" | "classic" | "node16" | "nodenext" | "bundler"
}
}
| 策略 | 适用场景 | 说明 |
|---|---|---|
bundler | Vite/Webpack等 | 最灵活,支持导出映射 |
node16/nodenext | Node.js ESM | 严格的ESM解析 |
node | Node.js CJS | 传统Node解析 |
4.2 路径映射
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"]
}
}
}
// 使用路径别名
import { Button } from '@components/Button';
import { formatDate } from '@utils/date';
import type { User } from '@/types';
4.3 导出映射(package.json exports)
// package.json
{
"name": "my-lib",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./utils": {
"types": "./dist/utils.d.ts",
"import": "./dist/utils.mjs"
}
}
}
5. 声明文件
5.1 编写声明文件
// env.d.ts - 全局类型声明
declare namespace NodeJS {
interface ProcessEnv {
NODE_ENV: 'development' | 'production' | 'test';
API_URL: string;
DATABASE_URL: string;
}
}
// 为没有类型的库编写声明
declare module 'untyped-lib' {
export function doSomething(value: string): number;
export const version: string;
interface Options {
timeout?: number;
retries?: number;
}
export function create(options?: Options): void;
}
// 扩展已有模块
declare module 'express' {
interface Request {
userId?: string;
sessionId?: string;
}
}
// 全局变量声明
declare const __APP_VERSION__: string;
declare const __DEV__: boolean;
5.2 三斜线指令
/// <reference path="types.d.ts" />
/// <reference types="node" />
/// <reference lib="es2020" />
// 三斜线指令用于:
// - 引入其他声明文件
// - 声明对@types包的依赖
// - 指定需要的lib
6. 常见问题与解决方案
6.1 模块与脚本的区分
// TypeScript根据文件内容判断是模块还是脚本
// 包含 import 或 export → 模块(文件作用域)
// 不包含 → 脚本(全局作用域)
// 空文件想要作为模块:添加空导出
export {};
6.2 循环依赖
// 问题:A导入B,B导入A
// a.ts
import { b } from './b';
export const a = 'a' + b; // b可能是undefined
// 解决方案1:重构,提取共享部分到第三个文件
// 解决方案2:使用延迟导入
export function getB() {
const { b } = require('./b'); // 运行时导入
return b;
}
// 解决方案3:使用接口解耦
6.3 CJS与ESM互操作
// tsconfig.json
{
"compilerOptions": {
"esModuleInterop": true, // 允许CJS默认导入
"allowSyntheticDefaultImports": true,
}
}
// CJS模块默认导入
import express from 'express'; // 需要esModuleInterop
import * as express from 'express'; // 不需要
// 在ESM中导入CJS
import { readFileSync } from 'fs'; // CJS具名导出
7. 总结与最佳实践
7.1 模块组织原则
- 使用ES模块:
import/export,避免命名空间 - 类型导入用
import type:明确区分类型和值 - 桶文件(index.ts):统一导出入口,但避免过度嵌套
- 路径别名:使用
@/前缀简化导入路径
7.2 最佳实践
- 新项目不用命名空间:ES模块是标准
- 声明文件集中管理:放在
src/env.d.ts或types/目录 - 避免循环依赖:重构代码结构
- 使用
exports字段:库项目配置导出映射 esModuleInterop: true:简化CJS互操作