声明合并进阶
本教程共 80 篇 · 第 60 篇 · 更新于 2026-08-10 · 约 10 分钟阅读
本节目标:掌握声明合并的进阶用法——模块增强(module augmentation)、全局增强、namespace 与 class/function/enum 的合并模式。同时了解合并的限制和边界,知道什么时候该克制使用这些技巧。
第 29 章讲过了 interface 的同名合并,那是最基础的声明合并形式。这一章我们深入更复杂的合并场景:跨模块增强、namespace 与值的合并、以及这些技巧在实际项目里的适用边界。
模块增强(Module Augmentation)
JavaScript 模块通常不支持”追加成员”——一个模块导出什么就是什么。但 TypeScript 的类型系统允许你在不修改原模块源码的情况下,给它的导出追加类型成员。这个机制叫模块增强(module augmentation)。
假设你引了一个第三方数学库:
// 第三方包 math-lib 的声明
export function add(a: number, b: number): number;
export function subtract(a: number, b: number): number;
你觉得 add 应该也支持数组求和,但改不了原包。用模块增强:
// math-lib-extensions.d.ts
declare module "math-lib" {
export function add(numbers: number[]): number;
}
这一行在已有的 math-lib 声明之上追加了一个 add 的新重载。原来的 add(a: number, b: number) 不受影响,现在 add 同时接受两个数字或一个数组。
Note模块增强只追加类型声明,不改变运行时行为。如果你的重载签名期望
add([1,2,3]),你需要在运行时也实现这个分支——TypeScript 不会帮你写实现。
再看一个更实际的例子。假设你想给 Observable 类加一个 map 方法:
// observable.ts——第三方库
export class Observable<T> {
subscribe(callback: (value: T) => void): void {
/* 实现略 */
}
}
// observable-extensions.ts——你的增强文件
import { Observable } from "./observable";
declare module "./observable" {
interface Observable<T> {
map<U>(fn: (value: T) => U): Observable<U>;
}
}
// 运行时补上实现
Observable.prototype.map = function <T, U>(this: Observable<T>, fn: (value: T) => U): Observable<U> {
const result = new Observable<U>();
this.subscribe((value) => {
// 简化实现
});
return result;
};
关键点:declare module "./observable" 的路径必须和 import 的路径完全一致。TypeScript 用它来定位”你想增强的是哪个模块”。interface Observable<T> 里追加的 map 方法会和原来的 Observable 类合并——因为类会为它的实例类型创建一个同名的 interface,模块增强里追加的 interface 成员正好合并进去。
模块增强有两条硬限制:
- 不能新增顶层导出。 只能给已有的导出追加成员,不能引入全新的
export function xxx或export class xxx。 - 不能增强默认导出。
export default的东西无法通过模块增强修改类型。
全局增强
从模块文件内部往全局类型加东西,用 declare global(第 59 章提过)。它和模块增强的思路一样,只不过目标不是某个模块,而是全局作用域:
// 在一个模块文件(有 import/export)里
declare global {
interface Array<T> {
toObservable(): Observable<T>;
}
interface String {
truncate(maxLength: number): string;
}
}
// 运行时补上实现
Array.prototype.toObservable = function <T>(this: T[]) {
return new Observable<T>();
};
export {}; // 确保文件是模块
declare global 和模块增强的规则一致:只追加,不覆盖;只声明类型,不提供实现。
Warning全局增强很强大,但也容易造成”全局污染”——你的代码无差别地修改了所有地方看到的
Array或String类型。团队项目里这种”猴子补丁”式的类型增强应该放在一个集中的.d.ts文件里,并加上注释说明为什么需要这样做。
namespace 合并 class、function、enum
这是 TypeScript 最”JavaScript 风格”的设计之一。JavaScript 里函数本身也能挂属性、类也有静态方法——namespace 可以给声明合并提供类型支持。
namespace 合并 class: 给类加”内部类”和额外的静态成员。
class Album {
label: Album.AlbumLabel;
}
namespace Album {
export class AlbumLabel {
constructor(public name: string) {}
}
}
const album = new Album();
album.label = new Album.AlbumLabel("TypeScript Hits");
namespace Album 里的 AlbumLabel 通过 Album.AlbumLabel 访问——就像 Java 的内部类。不过这里没有真正的”内部”关系,只是类型和命名空间上的合并。
namespace 合并 function: 描述”函数本身也是个对象、上面有属性”的 JS 模式。
function buildLabel(name: string): string {
return buildLabel.prefix + name + buildLabel.suffix;
}
namespace buildLabel {
export let suffix = "";
export let prefix = "Hello, ";
}
buildLabel.prefix = "Hi, ";
console.log(buildLabel("TypeScript")); // Hi, TypeScript
buildLabel 既是函数也是包含 prefix 和 suffix 属性的对象。没有 namespace 合并的话,你只能在 buildLabel 函数体里用 (buildLabel as any).prefix——类型安全完全丢掉。
namespace 合并 enum: 给枚举加工具方法。
enum Color {
red = 1,
green = 2,
blue = 4,
}
namespace Color {
export function mix(c1: Color, c2: Color): Color {
return c1 | c2; // 使用位运算混合颜色
}
export function getName(c: Color): string {
switch (c) {
case Color.red: return "红色";
case Color.green: return "绿色";
case Color.blue: return "蓝色";
default: return "未知";
}
}
}
const yellow = Color.mix(Color.red, Color.green);
console.log(yellow); // 3
console.log(Color.getName(Color.blue)); // 蓝色
这个模式在 enum 里特别实用——枚举值本身就是类型,namespace 追加的方法让枚举变成了一个功能完整的”枚举模块”。
合并限制
不是什么都能合并。TypeScript 的声明合并有明确的边界:
允许的合并:
interface+interfacenamespace+namespacenamespace+classnamespace+functionnamespace+enum- 模块增强(
declare module)追加已有导出的成员 - 全局增强(
declare global)追加全局类型的成员
不允许的合并:
class+class(同名类不能合并)class+variabletype别名 + 任何东西(type不参与合并)enum+enum(同名枚举不能合并)
如果确实需要”合并两个类”的效果,用 Mixin(第 37 章讲过)。
最佳实践:克制使用声明合并
声明合并很酷,但你不会希望自己的代码库变成别人猜谜的地方。几条实践建议:
业务代码里只用 interface 合并。 interface 的同名合并是声明合并里最无害的形式,广泛用于给第三方库的类型”打补丁”。其他合并形式——namespace 合并 class/function/enum——放声明文件里。
模块增强集中管理。 把所有 declare module 放在一个 types/extensions.d.ts 文件里,而不是散落在项目各处。这样新同事能一眼看到”我们对哪些包做了类型增强”。
先看有没有替代方案。 想给 Array 加方法?考虑写一个工具函数而不是修改原型。想给第三方模块追加类型?考虑提一个 PR 给 DefinitelyTyped 而不是在项目里 patch。
写注释。 声明合并的代码不那么直观。每个合并声明旁边写一句”为什么要这样做”,对三个月后的自己和同事都友好。
小结
声明合并是 TypeScript 类型系统里的一块”瑞士军刀”——模块增强让你在第三方代码上追加类型,全局增强让你安全地扩展内置类型,namespace 合并让类型系统能表达 JavaScript 里那些”函数也是对象”的惯用写法。但这些工具的共性是需要克制。合理使用它们,你的类型定义会很精准;滥用它们,你的项目会变成一个类型谜题。