前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >声明合并_TypeScript笔记16

声明合并_TypeScript笔记16

作者头像
ayqy贾杰
发布2019-06-12 15:17:08
1.1K0
发布2019-06-12 15:17:08
举报
文章被收录于专栏:黯羽轻扬黯羽轻扬

一.简介

类似于 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等全局声明中

参考资料

  • Declaration Merging
  • Mixins
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-04-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端向后 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一.简介
  • 二.基本概念
  • 三.合并接口
  • 四.合并命名空间
    • 与类、函数及枚举的合并
      • 与类合并
      • 与函数合并
      • 与枚举合并
  • 五.Class Mixin
  • 六.模块扩展
    • 全局扩展
      • 参考资料
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档