learn-typescript

声明合并

介绍

声明合并(declaration-merging), 是指编译器将多个相同名字的独立声明合并成一个声明,该声明具有原先所有声明的特性。

基础概念

TypeScript 中的声明会创建以下三种实体中的部分实体: 命名空间、类型或值。

声明 命名空间 类型
namespace  
class  
enum  
interface    
type alias    
function    
variable    

接口合并

接口合并最常见也最简单。合并接口的机制从根本上说,是把接口的成员放在一个同名的接口里,但也有一些规则:

  1. 非函数成员的名字应该是唯一的,或者具有相同类型:

    interface Box {
      height: number
      width: number
    }
    
    interface Box {
      scale: number
    }
    
    let box: Box = { height: 5, width: 3, scale: 4 }
    
  2. 同名的函数成员当作函数重载,一般情况下,合并时后面的接口具有更高的优先级,且每个接口中的声明顺序不变:

    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) {
  // ...
}

注意点:

  1. 模块拓展不能在拓展中声明新的顶级声明,仅可以拓展模块中已经存在的声明;
  2. 使用拓展的代码,需要引入原模块和拓展代码所在的模块:

    // 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() {
  // ...
}