一.简介
类似于 CSS 里的声明合并:
.box {
background: red;
}
.box {
color: white;
}
/* 等价于 */
.box {
background: red;
color: white;
}
TypeScript 也有这样的机制:
interface IPerson {
name: string;
}
interface IPerson {
age: number;
}
// 等价于
interface IPerson {
name: string;
age: number;
}
简言之,多条描述同一个东西的声明会被合并成一条
二.基本概念
TypeScript 里,一条声明可能会创建命名空间、类型或值,比如声明 Class 时会同时创建类型和值:
class Greeter {
static standardGreeting = "Hello, there";
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter: Greeter; // Greeter类型
greeter = new Greeter("world"); // Greeter值
(摘自类与类型)
因此,可以把声明分为 3 类:
会创建命名空间的声明:创建一个用点号(
.
)来访问的命名空间名会创建类型的声明:创建一个指定“形状”的类型,并以给定的名称命名
会创建值的声明:创建一个值,在输出的 JavaScript 中也存在
具体的,在 TypeScript 的 7 种声明中,命名空间具有命名空间和值含义,类与枚举同时具有类型和值含义,接口与类型别名只有类型含义,函数与变量只有值含义:
Declaration Type | Namespace | Type | Value |
---|---|---|---|
Namespace | X | X | |
Class | X | X | |
Enum | X | X | |
Interface | X | ||
Type Alias | X | ||
Function | X | ||
Variable | X |
三.合并接口
最简单,也最常见的声明合并就是接口合并,基本规则是把同名接口的成员放到一起:
interface Box {
height: number;
width: number;
}
interface Box {
scale: number;
}
// 等价于
interface MergedBox {
height: number;
width: number;
scale: number;
}
let box: Box = {height: 5, width: 6, scale: 10};
let b: MergedBox = box;
要求非函数成员唯一,如果不唯一的话,类型相同的函数成员会被忽略掉,类型不同的则抛出编译错误:
interface Box {
color: string
}
// 错误 Subsequent property declarations must have the same type.
interface Box {
color: number
}
对于函数成员,同名的看作函数重载:
class Animal { }
class Sheep { }
class Dog { }
class Cat { }
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 IDocument {
createElement(tagName: any): Element;
}
interface IDocument {
createElement(tagName: "div"): HTMLDivElement;
createElement(tagName: "span"): HTMLSpanElement;
}
interface IDocument {
createElement(tagName: string): HTMLElement;
createElement(tagName: "canvas"): HTMLCanvasElement;
}
合并结果为:
interface IDocument {
// 特殊签名置顶
createElement(tagName: "div"): HTMLDivElement;
createElement(tagName: "span"): HTMLSpanElement;
createElement(tagName: "canvas"): HTMLCanvasElement;
// 下面两条仍遵循后声明的优先
createElement(tagName: string): HTMLElement;
createElement(tagName: "canvas"): HTMLCanvasElement;
}
四.合并命名空间
类似于接口,多个同名命名空间也会发生成员合并,特殊之处在于命名空间还具有值含义,情况稍复杂一些
命名空间合并:各(同名)命名空间暴露出的接口进行合并,同时单个命名空间内部也进行接口合并
值合并:将后声明的命名空间中暴露出的成员添加到先声明的上
例如:
namespace Animals {
export class Zebra { }
}
namespace Animals {
export interface Legged { numberOfLegs: number; }
export class Dog { }
}
// 等价于
namespace Animals {
export interface Legged { numberOfLegs: number; }
export class Zebra { }
export class Dog { }
}
特殊的,未暴露出的成员(non-exported member)仍只在源命名空间可见(即便存在命名空间合并机制):
namespace Animal {
let haveWings = true;
export function animalsHaveWings() {
return haveWings;
}
}
namespace Animal {
export function doAnimalsHaveWings() {
// 错误 Cannot find name 'haveWings'.
return haveWings;
}
}
因为命名空间具有作用域隔离,未暴露出的成员不会被挂到命名空间上:
var Animal;
(function (Animal) {
var haveWings = true;
function animalsHaveWings() {
return haveWings;
}
Animal.animalsHaveWings = animalsHaveWings;
})(Animal || (Animal = {}));
(function (Animal) {
function doAnimalsHaveWings() {
// 错误 Cannot find name 'haveWings'.
return haveWings;
}
Animal.doAnimalsHaveWings = doAnimalsHaveWings;
})(Animal || (Animal = {}));
与类、函数及枚举的合并
除了能与其它命名空间合并外,命名空间还能与类、函数以及枚举合并
这种能力允许(在类型上)扩展现有类、函数与枚举,用于描述 JavaScript 中的常见模式,比如给类添加静态成员,给函数添加静态属性等等
P.S.要求命名空间声明必须后出现,否则报错:
// 错误 A namespace declaration cannot be located prior to a class or function with which it is merged.
namespace A {
function f() { }
}
class A {
fn() { }
}
因为会发生覆盖,而不是合并:
// 编译结果
var A;
(function (A) {
function f() { }
})(A || (A = {}));
var A = /** @class */ (function () {
function A() {
}
A.prototype.fn = function () { };
return A;
}());
与类合并
可以通过命名空间来给现有 Class 添加静态成员,例如:
class Album {
label: Album.AlbumLabel;
}
namespace Album {
export class AlbumLabel { }
}
与命名空间之间的合并规则一致,所以要暴露出class AlbumLabel
,允许其它声明中的成员访问
与函数合并
类似于命名空间与类的合并,与函数合并能够给现有函数扩展静态属性:
function buildLabel(name: string): string {
return buildLabel.prefix + name + buildLabel.suffix;
}
namespace buildLabel {
export let suffix = "";
export let prefix = "Hello, ";
}
// test
buildLabel('Lily') === "Hello, Lily"
与枚举合并
enum Color {
red = 1,
green = 2,
blue = 4
}
namespace Color {
export function mixColor(colorName: string) {
if (colorName == "yellow") {
return Color.red + Color.green;
}
else {
return -1;
}
}
}
// test
Color.mixColor('white');
让枚举拥有静态方法看起来比较奇怪,但 JavaScript 里确实存在类似的场景,相当于给属性集添加行为:
// JavaScript
const Color = {
red: 1,
green: 2,
blue: 4
};
Color.mixColor = function(colorName) {/* ... */};
五.Class Mixin
类声明不会与其它类或变量声明发生合并,但可以通过 Class Mixin 来达到类似的效果:
function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
Object.defineProperty(derivedCtor.prototype, name, Object.getOwnPropertyDescriptor(baseCtor.prototype, name));
});
});
}
通过工具函数把其它类原型上的东西都粘到目标类原型上去,使之拥有其它类的能力(行为):
class Editable {
public value: string;
input(s: string) { this.value = s; }
}
class Focusable {
focus() { console.log('Focused'); }
blur() { console.log('Blured'); }
}
// 从其它类获得类型
class Input implements Editable, Focusable {
// 待实现的 Editable 接口
value: string;
input: (s: string) => void;
// 待实现的 Focusable 接口
focus: () => void;
blur: () => void;
}
// 从其它类获得行为
applyMixins(Input, [Editable, Focusable]);
// log 'Focused'
new Input().focus();
P.S.其中implements Editable, Focusable
取源类的类型,类似于接口,具体见Interfaces Extending Classes
六.模块扩展
// 源码文件 observable.js
export class Observable {
constructor(source) { this.source = source; }
}
// 源码文件 map.js
import { Observable } from "./observable";
Observable.prototype.map = function (f) {
return new Observable(f(this.source));
}
这种模块扩展方式在 JavaScript 中很常见,但在 TypeScript 下会得到报错:
// 源码文件 observable.ts
export class Observable<T> {
constructor(public source: T) { }
}
// 源码文件 map.ts
import { Observable } from "./observable";
// 错误 Property 'map' does not exist on type 'Observable<any>'.
Observable.prototype.map = function (f) {
return new Observable(f(this.source));
}
此时可以通过模块扩展(module augmentation)告知编译器(类型系统)模块中新增的成员:
// 源码文件 map.ts
import { Observable } from "./observable";
// 模块扩展
declare module "./observable" {
interface Observable<T> {
map<U>(f: (x: T) => U): Observable<U>;
}
}
Observable.prototype.map = function (f) {/* ... */}
其中,模块名的解析方式与import/export
一致,具体见模块解析机制_TypeScript 笔记 14,而模块声明中新增的扩展成员会被合并到源模块中(就像本来就声明在同一个文件中一样)。能够以这种方式扩展现有模块,但有2 点限制:
无法在模块扩展中添加顶层声明,只能对扩展已存在的声明
无法扩展默认导出,只能扩展具名导出(因为
default
是保留字,无法按名扩展,具体见Can not declaration merging for default exported class)
P.S.上例在Playground等环境可能会遇到declare module "./observable"
报错:
Invalid module name in augmentation, module ‘./observable’ cannot be found.
Ambient module declaration cannot specify relative module name.
是模块文件不存在引起的,在真实文件模块中能够正常编译
全局扩展
也能以类似的方式扩展“全局模块”(即修正全局作用域下的东西),例如:
// 源码文件 observable.ts
export class Observable<T> {
constructor(public source: T) { }
}
declare global {
interface Array<T> {
toObservable(): Observable<T>;
}
}
Array.prototype.toObservable = function () {
return new Observable(this);
}
declare global
表示扩展全局作用域,新增的东西会被合并到Array
等全局声明中