首页 / TypeScript 入门教程 / 声明合并进阶

TypeScript 入门教程

声明合并进阶

本教程共 80 篇 · 第 60 篇 · 更新于 2026-08-10 · 约 10 分钟阅读

TypeScriptTypeScript 入门教程声明合并模块增强module augmentationdeclare module

本节目标:掌握声明合并的进阶用法——模块增强(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 成员正好合并进去。

模块增强有两条硬限制:

  1. 不能新增顶层导出。 只能给已有的导出追加成员,不能引入全新的 export function xxxexport class xxx
  2. 不能增强默认导出。 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

全局增强很强大,但也容易造成”全局污染”——你的代码无差别地修改了所有地方看到的 ArrayString 类型。团队项目里这种”猴子补丁”式的类型增强应该放在一个集中的 .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 既是函数也是包含 prefixsuffix 属性的对象。没有 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 + interface
  • namespace + namespace
  • namespace + class
  • namespace + function
  • namespace + enum
  • 模块增强(declare module)追加已有导出的成员
  • 全局增强(declare global)追加全局类型的成员

不允许的合并:

  • class + class(同名类不能合并)
  • class + variable
  • type 别名 + 任何东西(type 不参与合并)
  • enum + enum(同名枚举不能合并)

如果确实需要”合并两个类”的效果,用 Mixin(第 37 章讲过)。

最佳实践:克制使用声明合并

声明合并很酷,但你不会希望自己的代码库变成别人猜谜的地方。几条实践建议:

业务代码里只用 interface 合并。 interface 的同名合并是声明合并里最无害的形式,广泛用于给第三方库的类型”打补丁”。其他合并形式——namespace 合并 class/function/enum——放声明文件里。

模块增强集中管理。 把所有 declare module 放在一个 types/extensions.d.ts 文件里,而不是散落在项目各处。这样新同事能一眼看到”我们对哪些包做了类型增强”。

先看有没有替代方案。 想给 Array 加方法?考虑写一个工具函数而不是修改原型。想给第三方模块追加类型?考虑提一个 PR 给 DefinitelyTyped 而不是在项目里 patch。

写注释。 声明合并的代码不那么直观。每个合并声明旁边写一句”为什么要这样做”,对三个月后的自己和同事都友好。

小结

声明合并是 TypeScript 类型系统里的一块”瑞士军刀”——模块增强让你在第三方代码上追加类型,全局增强让你安全地扩展内置类型,namespace 合并让类型系统能表达 JavaScript 里那些”函数也是对象”的惯用写法。但这些工具的共性是需要克制。合理使用它们,你的类型定义会很精准;滥用它们,你的项目会变成一个类型谜题。