Skip to content

模块与命名空间(Modules & Namespaces)

核心问题解析

问题一:import type 和 import 有什么区别?

看这个例子:

ts
// user.ts
export interface User {
id: number;
name: string;
}
export const createUser = (name: string): User => ({ id: Date.now(), name });

在另一个文件中:

ts
// service.ts
import { User, createUser } from './user';

const user: User = createUser('Alice');

生成的 JavaScript:

js
// service.js
import { createUser } from './user'; // ❌ 运行时引入了 User(但 User 是类型!)
const user = createUser('Alice');

User 是编译时类型,不应该出现在运行时代码中。

解决方案:import type

ts
import type { User } from './user'; // ✅ 只在编译时引入
import { createUser } from './user';

const user: User = createUser('Alice');

生成的 JS:

js
import { createUser } from './user'; // ✅ 只有值被引入
const user = createUser('Alice');

核心区别:

语法用途是否生成 JS 代码
import { X }引入值或值+类型✅ 是
import type { X }仅引入类型❌ 否,编译后完全擦除

使用 import type 可避免:

  • 无意的运行时依赖
  • 循环引用问题
  • tree-shaking 失效

问题二:命名空间还能用吗?

命名空间(namespace) 是 TS 早期为解决全局变量污染而设计的机制:

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 实现按需、零开销引入。

ts
// 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 是例外:它既是类型又是值。

ts
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
最佳实践:拆分导入
ts
import type { User, Role } from './types';
import { createUser, assignRole } from './users';

// 类型和值清晰分离

错误示例:类型导致运行时依赖

ts
// bad.ts
import { HeavyComponent, Props } from 'ui-lib'; // 即使只用 Props,也会引入整个组件

const props: Props = { /.../ };

修正:

ts
import type { Props } from 'ui-lib'; // ✅ 无运行时开销
import { HeavyComponent } from 'ui-lib'; // 按需引入值

目标三:掌握模块解析策略(moduleResolution)

TS 如何根据 import 语句找到文件?这由 moduleResolution 配置决定。

策略说明适用场景
classicTS 旧版解析逻辑老项目
node模拟 Node.js 的 require 规则✅ 现代项目(推荐)
node16 / nodenext支持 ES Modules 和 package.json 的 exportsESM 项目

node 策略查找顺序(以 import 'lodash' 为例):

  1. ./node_modules/lodash.js
  2. ./node_modules/lodash/index.js
  3. 查找 ./node_modules/lodash/package.json 中的 main 字段
  4. 查找对应 .d.ts 类型文件

tsconfig.json 中设置:

json
{
   "compilerOptions": {
"moduleResolution": "node"
}
}

目标四:理解 declare namespace 在旧版库中的作用

@types/ 包中常见:

ts
// @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 修正:

ts
// 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 风格:

ts
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
  • /utils
  • format.ts
  • index.ts

format.ts 导出一个函数 formatDate。

index.ts 中,以下导入方式是否都能成功?为什么?

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,要求支持:

ts
Analytics.track('page_view');
Analytics.userId = '123';
Analytics.config.debug = true;

使用 declare namespace 实现。

练习题 5:类型与值的混合导入

class 既是类型又是值,如何正确导入?

ts
// user.ts
export class User {
constructor(public id: number, public name: string) {}
}

// service.ts
import type { User } from './user'; // ❌ 无法用 new User()
// 如何同时获得类型和值?

提供两种解决方案。