21.声明合并 副本.md 来源:https://my.feishu.cn/file/J8wEb98yeoUazOxovtucrKGLnuf 采集状态:已完成正文采集;已从查看代码模式逐行提取,行号 1 至 136 连续,并核对滚动到底。 在 TypeScript 的世界里,大多数时候我们遵循一个直观的规则:一个名字对应一个实体。但 TypeScript 藏着一个“炼金术”般的秘密,它允许我们将多个拥有**相同名称**的独立声明,由编译器自动“熔合”成一个单一的、更丰富的定义。这个过程,就是**声明合并 (Declaration Merging)**。 这并非一个 bug 或巧合,而是 TypeScript 为了与现有 JavaScript 的动态特性和模块化生态系统和谐共存而精心设计的一项功能。它赋予了 TypeScript 惊人的**可扩展性**,尤其是在增强已有类型或为第三方库提供类型定义时。 ### 什么是声明合并? 简单来说,声明合并是 TypeScript 编译器的一种能力,它会找到所有使用相同名称声明的实体,并将它们的属性和成员组合成一个统一的结构。可以把它想象成将同一建筑物的不同部分的蓝图(如电气图、管道图、结构图)叠加在一起,形成一张完整的总蓝图。 这个“炼金术”主要发生在以下几种实体之间。 ### 1. 接口合并 (Merging Interfaces) 这是声明合并最常见、也最实用的场景。当多个 `interface` 声明拥有相同的名称时,它们的成员列表会被合并。 **基础示例:** ```typescript interface Box { height: number; width: number; } interface Box { scale: number; } // 编译器会将其合并为: // interface Box { // height: number; // width: number; // scale: number; // } let box: Box = { height: 5, width: 6, scale: 10 }; ``` 对于方法成员,后声明的同名方法如果签名不同,会被视为该方法的重载。 **最强大的应用:扩展全局类型** 声明合并真正的威力在于它能让我们“安全地”扩展全局对象或第三方库的类型定义,而无需修改其源码。例如,我们可以为浏览器的全局 `window` 对象添加自定义属性: ```typescript // 在你的项目中的某个 .ts 文件里 declare global { interface Window { myAppConfig: { version: string; apiUrl: string; }; } } // 现在,你可以在代码中安全地访问这个新属性,并获得完整的类型提示 window.myAppConfig = { version: "1.0", apiUrl: "/api" }; ``` `declare global` 告诉 TypeScript,我们正在修改全局作用域。通过合并 `Window` 接口,我们为 `window` 对象添加了一个类型安全的 `myAppConfig` 属性。 ### 2. 命名空间合并 (Merging Namespaces) 与接口类似,同名的 `namespace` 也会将其导出的成员进行合并。这对于将一个大的命名空间拆分到多个文件中进行管理非常有用。 ```typescript // 文件: Animals.ts namespace Animals { export class Zebra {} } // 文件: Animals.extended.ts namespace Animals { export interface Legged { numberOfLegs: number; } export class Dog {} } // 编译器会将它们合并 let zebra = new Animals.Zebra(); let dog = new Animals.Dog(); ``` 合并后的 `Animals` 命名空间同时包含了 `Zebra` 和 `Dog`。命名空间合并是早期 TypeScript 代码组织的核心,但在现代模块化开发中,其地位已被 ES 模块取代。 ### 3. 命名空间与类/函数/枚举的合并 这是声明合并更高级、更精巧的应用。它允许我们将一个命名空间“附加”到一个类、函数或枚举上,为其添加额外的静态属性或相关的类型。这完美地模拟了 JavaScript 中函数也是对象的特性。 **命名空间与类合并:** ```typescript class Album { constructor(public label: string) {} } // 为 Album 类附加一个命名空间,用于存放相关的工具函数或类型 namespace Album { export function create(label: string) { return new Album(label); } export type AlbumFormat = 'CD' | 'Vinyl' | 'Digital'; } // 使用 const album = Album.create("My Album"); // 调用命名空间中的静态工厂方法 const format: Album.AlbumFormat = 'CD'; // 使用命名空间中的类型 ``` 通过这种方式,`Album` 同时作为一个可被 `new` 的类和一个包含静态成员的容器。 ### 规则的例外:什么不能合并? 理解什么能合并固然重要,但知道什么**不能合并**同样关键。 * **类型别名 (`type`) 不能合并**:这是 `type` 和 `interface` 的一个核心区别。类型别名创建的是一个确定的名称,不允许重复定义。 ```typescript // type Point = { x: number }; // type Point = { y: number }; // 编译时错误: Duplicate identifier 'Point'. ``` * **类不能合并**:你不能声明两个同名的类。 ### 为什么需要声明合并?其设计哲学 声明合并的存在,主要是为了解决 TypeScript 与现有 JavaScript 生态系统的兼容性问题。 1. **可扩展性 (Extensibility)**:它是 TypeScript 保持开放和可扩展的核心机制。它允许开发者在不修改原始代码的情况下,为现有的类型(尤其是全局类型或来自第三方库的类型)“打补丁”或添加新功能。 2. **模拟 JavaScript 模式**:JavaScript 中充满了“一个东西既是函数又是对象”的模式(例如 `jQuery` 的 `$`)。通过将 `function` 和 `namespace` 合并,TypeScript 能够精确地为这些动态模式建立类型模型。 3. **组织代码**:在某些设计模式中,将相关的类型、常量或工厂函数与主类放在同一个“命名实体”下,可以使代码组织得更内聚。 ### 总结 声明合并是 TypeScript 类型系统中一个独特而强大的特性,它体现了 TypeScript 在追求类型安全的同时,对 JavaScript 动态世界的深刻理解与尊重。 * 它能将多个同名的 **`interface`** 或 **`namespace`** 声明合并为一个。 * 它还能将 **`namespace`** “附加”到 **`class`**, **`function`**, 或 **`enum`** 上,为它们添加静态成员。 * 它最强大的应用场景是**扩展全局或第三方类型**,提供了无侵入式的类型增强能力。 * 与 `interface` 不同,**`type` 别名是最终的,不能被合并**。 在日常的应用开发中,我们可能不常手动创建复杂的合并,但理解其工作原理,能让我们在为第三方库编写或扩展声明文件(`.d.ts`)时游刃有余,并真正领会 TypeScript 类型系统设计的精妙之处。