第 107 章:命名空间与声明合并
学习目标
- 理解 namespace 的使用场景
- 掌握声明合并的各种形式
- 学会扩展第三方库类型
- 区分 namespace 与 module 的边界
一、namespace 基础
namespace 是 TS 早期为内部模块设计的一种组织代码的方式,类似其他语言的「包」概念。
1.1 基本用法
typescript
namespace Utils {
export function formatDate(d: Date): string {
return d.toISOString();
}
export class StringHelper {
static capitalize(s: string): string {
return s[0].toUpperCase() + s.slice(1);
}
}
export const VERSION = '1.0.0';
}
// 使用
Utils.formatDate(new Date());
Utils.StringHelper.capitalize('hello');
Utils.VERSION;1.2 嵌套 namespace
typescript
namespace App {
export namespace Components {
export class Button {}
export class Modal {}
}
export namespace Utils {
export function log() {}
}
}
// 使用
App.Components.Button;
App.Utils.log();1.3 跨文件 namespace
typescript
// a.ts
namespace App {
export class A {}
}
// b.ts
namespace App {
export class B {}
}
// 使用
namespace App {
const a = new A();
const b = new B();
}注意
跨文件 namespace 需要在编译时按顺序合并文件,已经过时。现代项目优先用 ES Module。
二、namespace vs module
typescript
// namespace(命名空间)
namespace MyApp {
export const foo = 1;
}
// module(模块)
export const foo = 1;| 特性 | namespace | module |
|---|---|---|
| 文件 | 通常单文件 | 一个文件即一个模块 |
| 现代项目 | ❌ 不推荐 | ✅ 推荐 |
| 适用场景 | 全局类型扩展、声明合并 | 业务代码 |
| 编译产物 | 立即执行 | 按需加载 |
现代项目建议
优先用 ES Module + 文件目录组织。namespace 仅在以下场景使用:
- 全局类型扩展
- 第三方库类型增强
三、声明合并(Declaration Merging)
声明合并是 TS 的核心特性:同名的多个声明自动合并。
3.1 接口合并
typescript
interface User {
name: string;
}
interface User {
age: number;
}
// 合并后
interface User {
name: string;
age: number;
}
const u: User = { name: 'Tom', age: 18 };3.2 namespace 合并
typescript
namespace Animals {
export class Cat {}
}
namespace Animals {
export class Dog {}
export const count = 0;
}
// 合并后
namespace Animals {
export class Cat {}
export class Dog {}
export const count: 0;
}3.3 namespace + interface 合并
typescript
// 声明:把函数和接口合并
interface MyFunction {
(x: number): string;
description: string;
}
function MyFunction(x: number): string {
return x.toString();
}
MyFunction.description = 'test';
// 也可以用 namespace + function
function greet(name: string): string {
return `Hello, ${name}`;
}
namespace greet {
export const description = 'greeting function';
}
greet('Tom'); // 调用
greet.description; // 访问属性3.4 class + interface 合并
typescript
class Box {
contents: string = '';
}
interface Box {
// 给类添加新方法(类型层面)
serialize(): string;
}
const b = new Box();
b.contents = 'hello';
b.serialize(); // ✅ 类型上有这个方法
// 但运行时需要手动实现:Box.prototype.serialize = function() { ... }四、模块扩展(Module Augmentation)
模块扩展是声明合并的一种特殊形式。
4.1 扩展第三方模块
typescript
// 给 express 的 Request 加 user 字段
declare global {
namespace Express {
interface Request {
user?: {
id: number;
name: string;
};
}
}
}
export {};
// 使用:req.user 自动有类型
app.get('/profile', (req, res) => {
console.log(req.user?.name);
});4.2 给已有模块加导出
typescript
// 假设 lodash 没有 myUtil,我们加上
import 'lodash';
declare module 'lodash' {
interface LoDashStatic {
myUtil(input: string): string;
}
}
// 现在 lodash 有 myUtil 了
_.myUtil('hello');4.3 扩展 React 模块
typescript
// 给 JSX 加自定义属性
declare module 'react' {
interface HTMLAttributes<T> {
'data-testid'?: string;
'data-cy'?: string;
}
}
// 现在 div 自动有这些属性
<div data-testid="main" /> // ✅ 有类型五、声明合并实战
5.1 给全局 window 加方法
typescript
// globals.d.ts
export {};
declare global {
interface Window {
myApp: {
version: string;
track(event: string, data?: object): void;
};
}
}
// 使用
window.myApp.track('click', { button: 'submit' });5.2 给已有类加方法(仅类型)
typescript
// augment.d.ts
declare global {
interface Array<T> {
toMyFormat(): string;
}
}
export {};
// 使用(类型上有,运行时需要自己实现)
[1, 2, 3].toMyFormat();5.3 扩展第三方 UI 库
typescript
// 扩展 Element Plus 组件属性
declare module 'element-plus' {
interface ButtonProps {
customProp?: string;
}
}
export {};六、namespace 的实际应用
虽然不推荐用于业务代码,但 namespace 在类型声明文件中仍有价值。
6.1 类型声明中的 namespace
typescript
// chart.d.ts
declare namespace ChartLib {
interface Config {
type: string;
data: any[];
}
class Chart {
constructor(config: Config);
render(): void;
}
function create(config: Config): Chart;
}
declare module 'chart-lib' {
export = ChartLib;
}6.2 全局工具函数
typescript
// utils.d.ts
declare namespace MyApp {
function formatDate(d: Date): string;
function parseDate(s: string): Date;
}
// 使用
MyApp.formatDate(new Date());七、模块声明 vs 命名空间
7.1 模块声明更现代
typescript
// ✅ 推荐:用 ES Module
// utils/index.ts
export function formatDate(d: Date): string { /* ... */ }
export function parseDate(s: string): Date { /* ... */ }
// 使用
import { formatDate, parseDate } from '@/utils';7.2 命名空间仍有用的场景
typescript
// 1. 全局类型扩展(必须用 declare global)
declare global {
interface Window { foo: string; }
}
// 2. 老的库用 namespace 组织类型(向后兼容)
declare namespace jQuery {
interface AjaxSettings { /* ... */ }
function ajax(settings: AjaxSettings): void;
}
// 3. 声明合并(只能用 namespace)
function greet(s: string): string { return s; }
namespace greet {
export const version = '1.0';
}八、namespace 与 module 的混用
typescript
// types.ts
export namespace MyTypes {
export interface User {
id: number;
name: string;
}
export type Status = 'active' | 'inactive';
}
// 使用
import { MyTypes } from './types';
const user: MyTypes.User = { id: 1, name: 'Tom' };注意
这种写法不被推荐,应该用单独的 type 和 interface。
九、实战:声明文件中的 namespace
9.1 jQuery 风格库
typescript
// jquery.d.ts
declare namespace JQuery {
interface AjaxSettings {
url: string;
method?: 'GET' | 'POST';
data?: any;
}
interface Promise<T> {
done(callback: (data: T) => void): this;
fail(callback: (error: any) => void): this;
}
function ajax<T = any>(settings: AjaxSettings): Promise<T>;
function $(selector: string): HTMLElement;
}
declare module 'jquery' {
export = JQuery;
}9.2 浏览器全局库
typescript
// chartjs.d.ts(挂载到 window.Chart)
declare namespace Chart {
interface Config {
type: string;
data: any;
}
class Chart {
constructor(ctx: CanvasRenderingContext2D, config: Config);
update(): void;
}
}
interface Window {
Chart: typeof Chart;
}十、声明合并的限制
10.1 不能合并 class
typescript
class User {}
class User {} // ❌ Duplicate identifier 'User'
// ✅ 用 namespace 扩展
class User {}
namespace User {
export const version = '1.0';
}
// 这是合法的(给类加静态属性)10.2 函数不能合并(只能 namespace 扩展)
typescript
function foo() {}
function foo() {} // ❌ Duplicate function implementation
// ✅ 用 namespace 添加属性
function foo() {}
namespace foo {
export const extra = 1;
}10.3 enum 不能合并
typescript
enum Color { Red }
enum Color { Blue } // ❌ Duplicate identifier 'Color'十一、常见错误
11.1 在 ES Module 中使用 namespace
typescript
// ❌ 不推荐:用 namespace 组织业务代码
namespace UserService {
export function getUser() {}
}
// ✅ 推荐:用模块
// user-service.ts
export function getUser() {}11.2 忘记 export {}
typescript
// ❌ 报错
declare global {
interface Window { foo: string; }
}
// Augmentations for the global scope can only be directly nested in external modules
// ✅ 解决
declare global {
interface Window { foo: string; }
}
export {};11.3 namespace 跨文件顺序问题
typescript
// a.ts
namespace App {
export const a = 1;
}
// b.ts
namespace App {
export const b = a + 1; // ⚠️ 依赖 a.ts 先编译
}
// 解决:用 import 替代
// b.ts
import './a';
namespace App {
export const b = a + 1;
}十二、本章小结
| 要点 | 关键 |
|---|---|
| namespace | 早期内部模块,现代项目不推荐 |
| 适用场景 | 全局扩展、声明合并 |
| 声明合并 | 同名 interface/namespace 自动合并 |
| 模块扩展 | declare module 给已有库加类型 |
| 全局扩展 | declare global 必须在模块文件中 |
| class+interface | 给类加方法(仅类型层面) |
| 优先选择 | ES Module + 文件组织 |
动手练习
- 全局扩展:扩展
Window接口,添加自定义全局方法 - 模块扩展:给 lodash 加一个
myUtil方法的类型 - 声明合并:定义两个同名 interface,验证自动合并
- namespace 函数扩展:给原生
Array加一个groupBy方法的类型
推荐阅读
- 📖 TypeScript Handbook - Declaration Merging — 声明合并
- 📖 TypeScript Handbook - Module Augmentation — 模块扩展
- 🌐 DefinitelyTyped — 社区类型仓库
下一章:第 108 章:tsconfig 详解 →