声明合并(declaration-merging), 是指编译器将多个相同名字的独立声明合并成一个声明,该声明具有原先所有声明的特性。
TypeScript 中的声明会创建以下三种实体中的部分实体: 命名空间、类型或值。
.) 符号来访问时使用的名字;| 声明 | 命名空间 | 类型 | 值 | 
|---|---|---|---|
| namespace | √ | √ | |
| class | √ | √ | |
| enum | √ | √ | |
| interface | √ | ||
| type alias | √ | ||
| function | √ | ||
| variable | √ | 
接口合并最常见也最简单。合并接口的机制从根本上说,是把接口的成员放在一个同名的接口里,但也有一些规则:
非函数成员的名字应该是唯一的,或者具有相同类型:
interface Box {
  height: number
  width: number
}
interface Box {
  scale: number
}
let box: Box = { height: 5, width: 3, scale: 4 }
同名的函数成员当作函数重载,一般情况下,合并时后面的接口具有更高的优先级,且每个接口中的声明顺序不变:
interface Cloner {
  clone(animal: Animal): Animal
}
interface Cloner {
  clone(animal: Sheep): Sheep
}
interface Cloner {
  clone(animal: Dog): Dog
  clone(animal: Cat): Cat
}
将会合并成一个声明:
interface Cloner {
  clone(animal: Dog): Dog
  clone(animal: Cat): Cat
  clone(animal: Sheep): Sheep
  clone(animal: Animal): Animal
}
特殊情况:当函数签名有参数类型为 单一的字符串字面量 (不是字符串字面量的联合类型),那么该函数签名将提升到重载列表顶端:
interface Document {
  createElement(tagName: any): Element
}
interface Document {
  createElement(tagName: 'div'): HTMLDivElement
  createElement(tagName: 'span'): HTMLSpanElement
}
interface Document {
  createElement(tagName: string): HTMLElement
  createElement(tagName: 'canvas'): HTMLCanvasElement
}
合并后的接口声明为:
interface Document {
  createElement(tagName: 'canvas'): HTMLCanvasElement
  createElement(tagName: 'div'): HTMLDivElement
  createElement(tagName: 'span'): HTMLSpanElement
  createElement(tagName: string): HTMLElement
  createElement(tagName: any): Element
}
同名的命名空间会合并其中的成员,其中成员按照各自的合并规则。
命名空间中非导出成员(没有 export 出来的)仅仅在其原有的(合并前的)命名空间中可访问:
namespace Animal {
  let hasMuscles = true
  export function animalHasMuscles() {
    return hasMuscles
  }
}
namespace Animal {
  export function doAnimalHasMuscles() {
    // return hasMuscles // Error
    return animalHasMuscles() // ok
  }
}
命名空间可以与类、函数、枚举类型的声明进行合并。
命名空间可以和类合并,可以实现 内部类 和 静态属性 的效果:
class Album {
  label = new Album.AlbumLabel() // 内部类
  value = Album.staticValue // 静态属性
}
namespace Album {
  export class AlbumLabel {
    name: string = 'default label'
  }
  export const staticValue = '123'
}
命名空间可以和函数合并来拓展函数,允许创建一个函数然后拓展它增加一些属性:
function jQuery(arg: string): any {
  return `using jQuery('${arg}')`
}
namespace jQuery {
  export namespace fn {
    export const version: string = '0.0.1'
  }
}
let $ = jQuery
console.log(jQuery('body'), jQuery.fn.version) // using jQuery('body')  0.0.1
类似命名空间拓展函数, 命名空间可以和枚举类型合并来拓展枚举类型:
enum Color {
  Red,
  Blue,
  Green
}
namespace Color {
  export function mixColor(colorName: 'Yellow' | 'White') {
    if (colorName === 'Yellow') {
      return Color.Red + Color.Green
    } else {
      return Color.Red + Color.Blue + Color.Green
    }
  }
}
typescript 支持为模块打补丁来为模块进行拓展。
导入另一个模块的变量时,直接对该变量添加属性会报错,因为没有该变量的声明:
// observable.ts
export class Observable<T> {}
// map.ts
import { Observable } from './observable'
Observable.prototype.map = function(f) {}
// Error, Property 'map' does not exist on type 'Observable<any>'
此时,如果不想修改原来模块的代码,可以使用 模块拓展 , 内部配合声明合并来拓展模块中已有的代码:
// map.ts
import { Observable } from './observable'
declare module './observable' {
  interface Observable<T> {
    map<U>(f: (item: T) => U): Observable<U>
  }
}
Observable.prototype.map = function(f) {
  // ...
}
注意点:
使用拓展的代码,需要引入原模块和拓展代码所在的模块:
// consumer.ts
// 注意:需要引入 ./map.ts
import { Observable } from './observable'
import './map'
let o: Observable<number>
o.map(x => x.toFixed())
如果想要拓展全局作用域的功能,可以使用 declare gloal {} 来进行全局拓展:
declare global {
  interface Array<T> {
    toObservable(): Observable<T>
  }
}
Array.prototype.toObservable = function() {
  // ...
}