声明合并(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() {
// ...
}