TypeScript 进阶 声明合并
声明合并”是指编译器将针对同一个名字的两个独立声明合并为单一声明, 合并后的声明同时拥有原先两个声明的特性。 任何数量的声明都可被合并,不局限于两个声明。
基础概念
声明 会创建以下三种实体之一: 命名空间 , 类型 或 值 。
- 创建命名空间的声明会新建一个命名空间,它包含了用(.)符号来访问时使用的名字。
- 创建类型的声明是:用声明的模型创建一个类型并绑定到给定的名字上。
- 创建值的声明会创建在 JavaScript 输出中看到的值。
理解每个声明创建了什么,有助于理解当声明合并时有哪些东西被合并了。
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;
}
let box: Box = {height: 5, width: 6, scale: 10};
- 接口的非函数的成员应该是唯一的,如果不是唯一的,那么它们必须是相同的类型。否则编译器会报错
- 对于接口的函数成员,每个同名函数声明都会被当成这个函数的一个重载。 同时需要注意,当接口
A
与后来的接口A
合并时,后面的接口具有更高的优先级。
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;
}
合并命名空间
与接口相似,同名的命名空间也会合并其成员。 命名空间会创建出命名空间和值。
- 对于 命名空间的合并 ,模块导出的同名接口进行合并,构成单一命名空间内含合并后的接口。
- 对于 命名空间里值的合并 ,如果当前已经存在给定名字的命名空间,那么后来的命名空间的导出成员会被加到已经存在的那个模块里。
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 Animal {
let haveMuscles = true;
export function animalsHaveMuscles() {
return haveMuscles;
}
}
namespace Animal {
export function doAnimalsHaveMuscles() {
return haveMuscles; // Error, 因为这里无法访问 haveMuscles
}
}
命名空间与类、函数、枚举类型合并
命名空间可以与其它类型的声明进行合并,只要命名空间的定义符合将要合并类型的定义,合并结果包含两者的声明类型。 TypeScript 使用这个功能去实现一些 JavaScript 里的设计模式。
合并命名空间和类
# 内部类的模式
// 合并结果是一个类并带有一个内部类。 也可以使用命名空间为类增加一些静态属性。
class Album {
label: Album.AlbumLabel;
}
namespace Album {
export class AlbumLabel { }
}
合并命名空间和函数
# 创建一个函数稍后扩展它增加一些属性,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"));
合并命名空间和枚举类型
# 扩展枚举型
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;
}
}
}
模块扩展
虽然 JavaScript 不支持合并,但你可以为导入的对象打补丁以更新它们。
// observable.js
export class Observable<T> {
}
// map.js
import { Observable } from "./observable";
Observable.prototype.map = function (f) {
}
编译器对 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
解析模块标识符的方式是一致的。当这些声明在扩展中合并时,就好像在原始位置被声明了一样。 但是,你不能在扩展中声明新的顶级声明-仅可以扩展模块中已经存在的声明 。
全局扩展
全局扩展与模块扩展的行为和限制是相同的 。
在模块内部添加声明到全局作用域中:
// observable.ts
export class Observable<T> {
// ... still no implementation ...
}
declare global {
interface Array<T> {
toObservable(): Observable<T>;
}
}
Array.prototype.toObservable = function () {
// ...
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论