百科狗-知识改变命运!
--

声明合并 - TypeScript 标注类型

是丫丫呀1年前 (2023-11-21)阅读数 38#技术干货
文章标签声明

声明合并

TypeScript 中有些独特的概念可以在类型层面上描述 JavaScript 对象的模型。这其中尤其独特的一个例子是“声明合并”的概念。理解了这个概念,将有助于操作现有的 JavaScript 代码。同时,也会有助于理解更多高级抽象的概念。

对本文件来讲,“声明合并”是指编译器将针对同一个名字的两个独立声明合并为单一声明。合并后的声明同时拥有原先两个声明的特性。任何数量的声明都可被合并;不局限于两个声明。


基础概念

TypeScript 中的声明会创建以下三种实体之一:命名空间,类型或值。创建命名空间的声明会新建一个命名空间,它包含了用.符号来访问时使用的名字。创建类型的声明是:用声明的模型创建一个类型并绑定到给定的名字上。最后,创建值的声明会创建在 JavaScript 输出中看到的值。

Declaration TypeNamespaceTypeValue
NamespaceXX
ClassXX
EnumXX
InterfaceX
Type AliasX
FunctionX
VariableX

理解每个声明创建了什么,有助于理解当声明合并时有哪些东西被合并了。


合并接口

最简单也最常见的声明合并类型是接口合并。从根本上说,合并的机制是把双方的成员放到一个同名的接口里。

interface Box {
  height: number;
  width: number;
}

interface Box {
  scale: number;
}

let box: Box = { height: 5, width: 6, scale: 10 };

声明合并 - TypeScript 标注类型

接口的非函数的成员应该是唯一的。如果它们不是唯一的,那么它们必须是相同的类型。如果两个接口中同时声明了同名的非函数成员且它们的类型不同,则编译器会报错。


对于函数成员,每个同名函数声明都会被当成这个函数的一个重载。同时需要注意,当接口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;
}

合并后的Document将会像下面这样:

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, because haveMuscles is not accessible here }
}

因为haveMuscles并没有导出,只有animalsHaveMuscles函数共享了原始未合并的命名空间可以访问这个变量。doAnimalsHaveMuscles函数虽是合并命名空间的一部分,但是访问不了未导出的成员。


命名空间与类、函数、枚举类型合并

命名空间可以与其它类型的声明进行合并。只要命名空间的定义符合将要合并类型的定义。合并结果包含两者的声明类型。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"));

相似的,命名空间可以用来扩展枚举型:

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;
    }
  }
}


非法的合并

TypeScript 并非允许所有的合并。目前,类不能与其它类、变量合并。想要了解如何模仿类的合并,请参考 TypeScript 的混入。


模块扩展

虽然 JavaScript 不支持合并,但你可以为导入的对象打补丁以更新它们。让我们考察一下这个玩具性的示例:

// observable.ts
export class Observable {
  // ... implementation left as an exercise for the reader ...
}
// map.ts
import { Observable } from "./observable";
Observable.prototype.map = function (f) {
  // ... another exercise for the reader
};

它也可以很好地在 TypeScript 中工作,但编译器对Observable.prototype.map一无所知。你可以使用扩展模块来将它告诉编译器:

// observable.ts
export class Observable {
  // ... implementation left as an exercise for the reader ...
}
// map.ts
import { Observable } from "./observable";

declare module "./observable" {
  interface Observable {
    map(f: (x: T) => U): Observable;
  }
}

Observable.prototype.map = function (f) {
  // ... another exercise for the reader
};
// consumer.ts
import { Observable } from "./observable";
import "./map";

let o: Observable;
o.map((x) => x.toFixed());

模块名的解析和用import/export解析模块标识符的方式是一致的。当这些声明在扩展中合并时,就如同在原始位置被声明一样。但是,有两点限制需要注意:

  1. 你不能在扩展中声明新的顶级声明-仅可以扩展模块中已经存在的声明。
  2. 默认导出也不能扩展,只有命名的导出才可以(因为你需要使用导出的名字来进行扩展,并且default是保留关键字)


全局扩展

你也以在模块内部添加声明到全局作用域中。

// observable.ts
export class Observable {
  // ... still no implementation ...
}

declare global {
  interface Array {
    toObservable(): Observable;
  }
}

Array.prototype.toObservable = function () {
  // ...
};

全局扩展与模块扩展的行为和限制是相同的。

鹏仔微信 15129739599 鹏仔QQ344225443 鹏仔前端 pjxi.com 共享博客 sharedbk.com

免责声明:我们致力于保护作者版权,注重分享,当前被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理!邮箱:344225443@qq.com)

图片声明:本站部分配图来自网络。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!

内容声明:本文中引用的各种信息及资料(包括但不限于文字、数据、图表及超链接等)均来源于该信息及资料的相关主体(包括但不限于公司、媒体、协会等机构)的官方网站或公开发表的信息。部分内容参考包括:(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供参考使用,不准确地方联系删除处理!本站为非盈利性质站点,本着为中国教育事业出一份力,发布内容不收取任何费用也不接任何广告!)