模块与命名空间(Modules & Namespaces)
核心问题解析
问题一:import type 和 import 有什么区别?
看这个例子:
// user.ts
export interface User {
id: number;
name: string;
}
export const createUser = (name: string): User => ({ id: Date.now(), name });在另一个文件中:
// service.ts
import { User, createUser } from './user';
const user: User = createUser('Alice');生成的 JavaScript:
// service.js
import { createUser } from './user'; // ❌ 运行时引入了 User(但 User 是类型!)
const user = createUser('Alice');User 是编译时类型,不应该出现在运行时代码中。
解决方案:import type
import type { User } from './user'; // ✅ 只在编译时引入
import { createUser } from './user';
const user: User = createUser('Alice');生成的 JS:
import { createUser } from './user'; // ✅ 只有值被引入
const user = createUser('Alice');核心区别:
| 语法 | 用途 | 是否生成 JS 代码 |
|---|---|---|
import { X } | 引入值或值+类型 | ✅ 是 |
import type { X } | 仅引入类型 | ❌ 否,编译后完全擦除 |
使用 import type 可避免:
- 无意的运行时依赖
- 循环引用问题
tree-shaking失效
问题二:命名空间还能用吗?
命名空间(namespace) 是 TS 早期为解决全局变量污染而设计的机制:
namespace Utils {
export function log(message: string) { console.log(message); }
export const VERSION = '1.0';
}调用:Utils.log('hello')
现状:
- 可以使用,TS 仍完全支持
- 不推荐用于新项目
- 主要用途:封装旧版全局库的类型定义
- 现代项目使用 ES Modules(import/export)替代命名空间。
问题三:类型怎么做到按需引入?
我们希望:
只引入用到的类型,避免加载整个文件 类型不产生运行时开销
答案:通过 模块化 + import type 实现按需、零开销引入。
// types/user.ts
export type User = { id: number; name: string; };
export type Admin = User & { role: 'admin' };
// service.ts
import type { User } from './types/user'; // ✅ 只引入 User 类型
// 不会加载 Admin 类型,也不会生成运行时代码配合构建工具(如 Webpack、Vite),可实现真正的 tree-shaking。
学习目标详解
目标一:理解类型与值在模块中的分离
TypeScript 中,类型和值是两个独立的概念:
|类别 示例 |存在于运行时?| |值(Value) |function, const, class 实例| ✅ 是| |类型(Type)| interface, type, class 类型| ❌ 否(编译后擦除)|
但 class 是例外:它既是类型又是值。
class User {
constructor(public id: number, public name: string) {}
}
const u = new User(1, 'Alice'); // 值:构造函数
const v: User = u; // 类型:实例类型关键原则:在模块导入时,应明确区分你引入的是“类型”还是“值”。
目标二:使用 import type 避免运行时引入
何时使用 import type?
| 场景 | 推荐语法 |
|---|---|
| 只用于类型注解 | import type |
| 既用作类型又用作值 | import { T }(或拆分) |
| 防止循环依赖 | import type |
| 最佳实践: | 拆分导入 |
import type { User, Role } from './types';
import { createUser, assignRole } from './users';
// 类型和值清晰分离错误示例:类型导致运行时依赖
// bad.ts
import { HeavyComponent, Props } from 'ui-lib'; // 即使只用 Props,也会引入整个组件
const props: Props = { /.../ };修正:
import type { Props } from 'ui-lib'; // ✅ 无运行时开销
import { HeavyComponent } from 'ui-lib'; // 按需引入值目标三:掌握模块解析策略(moduleResolution)
TS 如何根据 import 语句找到文件?这由 moduleResolution 配置决定。
| 策略 | 说明 | 适用场景 |
|---|---|---|
| classic | TS 旧版解析逻辑 | 老项目 |
| node | 模拟 Node.js 的 require 规则 | ✅ 现代项目(推荐) |
| node16 / nodenext | 支持 ES Modules 和 package.json 的 exports | ESM 项目 |
node 策略查找顺序(以 import 'lodash' 为例):
./node_modules/lodash.js./node_modules/lodash/index.js- 查找
./node_modules/lodash/package.json中的 main 字段 - 查找对应 .
d.ts类型文件
在 tsconfig.json 中设置:
{
"compilerOptions": {
"moduleResolution": "node"
}
}目标四:理解 declare namespace 在旧版库中的作用
在 @types/ 包中常见:
// @types/jquery/index.d.ts
declare namespace jQuery {
function ajax(url: string): void;
const version: string;
namespace fn {
function myPlugin(): void;
}
}
export = jQuery;作用:
- 为全局库(如 jQuery、Lodash)声明复杂的全局结构
- 允许通过
jQuery.ajax()、jQuery.fn.myPlugin()等方式调用 - declare namespace 不生成任何 JS 代码,仅用于类型声明
现代库应使用 ES Modules 导出,但 declare namespace 对兼容旧库至关重要。
总结:本节核心要点
| 概念 | 关键点 |
|---|---|
| import type | 仅引入类型,零运行时开销 |
| namespace | 旧版封装方式,新项目用 ES Modules |
| 类型分离 | 明确区分类型与值的引入 |
| moduleResolution | 推荐 node 或 nodenext |
| declare namespace | 为全局库编写类型声明的必备工具 |
练习题
练习题 1:使用 import type 修正代码
以下代码生成了不必要的运行时依赖,请使用 import type 修正:
// api.ts
export interface ApiResponse<T> {
data: T;
status: number;
}
export const fetchData = <T>(url: string): Promise<ApiResponse<T>> => {
return fetch(url).then(r => r.json());
};
// component.ts
import { ApiResponse, fetchData } from './api';
async function loadUser() {
const res: ApiResponse<User> = await fetchData<User>('/user');
return res.data;
}如何修改 component.ts 以避免运行时引入 ApiResponse?
练习题 2:命名空间 vs 模块
将以下命名空间改写为 ES Modules 风格:
namespace MathUtils {
export const PI = 3.14159;
export function circleArea(r: number): number {
return PI r r;
}
export function sphereVolume(r: number): number {
return (4/3) PI r**3;
}
}要求:
- 使用
math.ts文件 - 支持按需导入(如只导入 circleArea)
练习题 3:模块解析路径
假设项目结构如下:
/src/utilsformat.tsindex.ts
format.ts 导出一个函数 formatDate。
在 index.ts 中,以下导入方式是否都能成功?为什么?
import { formatDate } from './utils/format';
import { formatDate } from './utils/format.ts';
import { formatDate } from 'utils/format';提示:检查 tsconfig.json 的 baseUrl 和 paths 配置。
练习题 4:declare namespace 实战
为一个全局库 Analytics 编写类型声明文件 analytics.d.ts,要求支持:
Analytics.track('page_view');
Analytics.userId = '123';
Analytics.config.debug = true;使用 declare namespace 实现。
练习题 5:类型与值的混合导入
class 既是类型又是值,如何正确导入?
// user.ts
export class User {
constructor(public id: number, public name: string) {}
}
// service.ts
import type { User } from './user'; // ❌ 无法用 new User()
// 如何同时获得类型和值?提供两种解决方案。