TypeScript 3.1 声明合并
一、介绍
TypeScript 中有一些独特的概念可以在类型层面上描述 JavaScript 对象的模型,这其中尤为独特的一个例子是"声明合并"的概念。
理解这个概念,有利于操作现有的 JavaScript 代码,同时,也有助于理解更多高级抽象的概念。
“声明合并” 指的是编译器将针对同一个名字的两个独立声明合并为单一声明。合并后的声明同时拥有原先两个声明的特性,任何数量的声明都可以被合并,比局限于两个。
二、基础概念
TypeScript 的声明会创建三种实体之一:namespace 、Type 或者 value,创建 namespace 的声明会新建一个 namespace,包含了用(.
)符号来访问时使用的名字。
创建 Type 的声明是:用声明的模型创建一个 class 并绑定到给定的名字上。最后创建 value 的声明会创建在 JavaScript 输出中看到的值。
声明类型 | Namespace | Type | Value |
---|---|---|---|
Namespace | x | | x |
Class | | x | x |
Enum | | x | x |
Interface | x | | |
Type Alias | | | x |
Function | | | x |
Variable | | | x |
理解每个声明创建了什么,有助于理解当声明合并的时候,那些东西被合并了。
三、合并 interface
最简单也是最常见的声明类型是接口合并,从本质上说,合并的机制是把双方的成员放到一个同名的接口中。
interface Box {
height: number;
width: number;
}
interface Box {
scale: number;
}
let b:Box = {height: 5, width: 6, scale: 1};
接口的非函数的成员应该是唯一的。如果它们不是唯一的,则它们必须是同一个类型。如果两个接口中同时声明了同名的非函数成员并且它们的类型不同,则编译器会报错。
对于函数成员,每个同名函数声明都会被当成这个函数的一个重载,需要注意的是,当接口 A 和后来的接口 A 合并时,后面的接口具有更好的优先级。
比如下面的示例:
interface Cloner {
clone(animal: Animal): Animal;
}
interface Cloner {
clone(animal: Animal): 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;
}
四、合并 namespace
和接口差不多,同名的 namespace 也会合并其成员。namespace 会创建出 namespace 和 value,我们需要知道这两者是怎么合并的。
对于 namespace 的合并,模块导出的同名接口进行合并,构成单一的命名空间内含合并后的接口。
Animals 声明合并示例:
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 { }
}
除了常见的合并完,还需要了解非导出成员是如何处理的,非导出成员仅在其原有的(合并前)的 namespace 中可见。这就是说合并之后,从其他命名空间合并进来的成员无法访问非导出成员。
下面的例子有更清晰的说明:
namespace Animal {
let haveMuscles = true;
export function animalsHaveMuscles() {
return haveMuscles;
}
}
namespace Animal {
export function doAnimalsHaveMuscles() {
return haveMuscles; // Error, because haveMuscles is not accessible here
}
}
因为 haveMuscles
并没有导出,只有 animalsHaveMuscles
函数共享了原始未合并的命名空间可以访问这个变量。
而 doAnimalsHaveMuscles
函数虽然是合并后的 Animal 的一部分,但是并不能访问未导出的成员。
五、命名空间与 class 和 function 和 Enum 的合并
命名空间可以与其他类型的声明进行合并。主要命名空间的定义符合将要合并类型的定义。
合并结果包含两者的声明类型。TypeScript 使用这个功能去实现一些 JavaScript 里的设计模式。
合并命名空间和类
这种方式能够去表示内部类:
class Album {
label: Album.AlbumLabel;
}
namespace Album {
export class AlbumLabel { }
}
合并规则与上面 合并命名空间
规则一致,必须导出 AlbumLabel
类,好让合并的类能够访问。合并结果是一个类并且带有一个内部类。
也可以使用命名空间为类增加一些静态属性。
除了内部类的模式,在 JavaScript 里,创建一个函数稍后扩展它增加一些属性也是很常见的。TypeScript 使用声明合并来达到这个目的并且保证类型安全。
function buildLabel(name: string): string {
return buildLabel.prefix + name + buildLabel.suffix;
}
namespace buildLabel {
export let suffix = "";
export let prefix = "Hello, ";
}
console.log(buildLabel("Sam Smith"));
编译后的结果:
function buildLabel(name) {
return buildLabel.prefix + name + buildLabel.suffix;
}
(function (buildLabel) {
buildLabel.suffix = "";
buildLabel.prefix = "Hello, ";
})(buildLabel || (buildLabel = {}));
console.log(buildLabel("Sam Smith"));
运行结果:
类似的,命名空间可以用来扩展枚举类型:
enum Color {
red = 1,
green = 2,
blue = 4
}
namespace Color {
export function mixColor(colorName: string) {
if (colorName === 'yellow') {
return Color.red + Color.green;
}
else if (colorName === 'white') {
return Color.red + Color.green + Color.blue;
}
else if (colorName == "magenta") {
return Color.red + Color.blue;
}
else if (colorName == "cyan") {
return Color.green + Color.blue;
}
}
}
编译结果:
var Color;
(function (Color) {
Color[Color["red"] = 1] = "red";
Color[Color["green"] = 2] = "green";
Color[Color["blue"] = 4] = "blue";
})(Color || (Color = {}));
(function (Color) {
function mixColor(colorName) {
if (colorName === 'yellow') {
return Color.red + Color.green;
}
else if (colorName === 'white') {
return Color.red + Color.green + Color.blue;
}
else if (colorName == "magenta") {
return Color.red + Color.blue;
}
else if (colorName == "cyan") {
return Color.green + Color.blue;
}
}
Color.mixColor = mixColor;
})(Color || (Color = {}));
六、非法的合并
TypeScript 并非允许所有的合并。 目前,class 不能与其它 class 或变量合并。
想要了解如何模仿类的合并,需要参考 TypeScript的混入。
七、模块扩展
虽然 JavaScript 不支持合并,但是你可以为导入的对象打补丁来更新它们。
// observable.js
export class Observable<T> {
// ...
}
// map.js
import { Observable } from "./observable";
Observable.prototype.map = function (f) {
// ... another exercise for the reader
}
它也可以很好地工作在 TypeScript 中, 但编译器对 Observable.prototype.map
一无所知。
你可以使用扩展模块把它告诉编译器:
// observable.ts stays the same
// 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) {
// ... another exercise for the reader
}
// consumer.ts
import { Observable } from "./observable";
import "./map";
let o: Observable<number>;
o.map(x => x.toFixed());
模块名的解析和用 ·import/ export· 解析模块标识符的方式是一致的。
更多信息请参考 Modules。
当这些声明在扩展中合并时,就好像在原始位置被声明了一样。 但是,你不能在扩展中声明新的顶级声明-仅可以扩展模块中已经存在的声明。
八、全局扩展
你也以在模块内部添加声明到全局作用域中。
// observable.ts
export class Observable<T> {
// ... still no implementation ...
}
declare global {
interface Array<T> {
toObservable(): Observable<T>;
}
}
Array.prototype.toObservable = function () {
// ...
}
全局扩展与模块扩展的行为和限制是相同的。
文章版权:Postbird-There I am , in the world more exciting!
本文链接:http://www.ptbird.cn/typescript-declaration-merging.html
转载请注明文章原始出处 !