TypeScript 的努力:声明合并

1,072 阅读3分钟

“声名合并” 是指编译器将对程序中多处出现的同一名字的两个及以上独立声名合并为单一声名,合并后的声名将具有原先所有独立声名的特性。

TypeScript 中的声明会创建以下三种实体之一:命名空间,类型或值。

  • 创建命名空间的声明会创建一个新的命名空间。
  • 创建类型的声明:用声明的模型创建一个类型,再绑定到给出的名字上。
  • 创建值的声明,会创建 js 在输出时看到的值。

合并接口

这是 ts 中最常见的声明合并。根本的合并机制是把双方的成员放在同一命名的接口中。

interface Person {
  name: string;
}
interface Person {
  age: number;
  name: number; // error: 接口的非函数的成员应该是唯一的,或者类型相同。
}
class person implements Person {
  name: string = "";
  age: number = 18;
}

对于接口的函数成员,每个同名函数都会被当成是该函数的一个重载。重载的 优先级规则 如下:

  • 每组的优先级顺序不变。
  • 后来的接口有更高的优先级
  • 当函数有一个参数的类型为单一的字符串字面量,该声明会被提到最顶端。
interface Person {
  like(sth: string): string; // 5
  like(sth: "dog"): any; // 2
}
interface Person {
  like(sth: number): number; // 3
  like(sth: string[]): string[]; // 4
  like(sth: "cat"): any; // 1
}

// 合并后
interface Person {
  like(sth: "cat"): any;
  like(sth: "dog"): any;
  like(sth: number): number;
  like(sth: string[]): string[];
  like(sth: string): string;
}

合并命名空间

命名空间合并规则:

  • 导出的成员不可以重复定义,这与接口合并不同。
  • 非导出成员只在原命名空间内可见。(从其它命名空间合并进来的成员无法访问非导出成员。)
namespace Animal {
  let haveMuscles = true;

  export function animalsHaveMuscles() {
    return haveMuscles;
  }
}

namespace Animal {
  export function doAnimalsHaveMuscles() {
    return haveMuscles; // Error, because haveMuscles is not accessible here
  }
}

// 合并后
namespace Animal {
  export function animalsHaveMuscles() {
    return haveMuscles;
  }
  export function doAnimalsHaveMuscles() {
    return haveMuscles;
  }
}

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

合并命名空间和类,可以让我们表示内部类,也可以为类添加静态属性

class Person {}
namespace Person {
  export let id = 123123;
  export class Man {}
}
console.log(Person.id);
console.log(Person.Man);

创建一个函数稍后扩展它增加一些属性。

function Func() {}
namespace Func {
  export let version = "1.0";
}
console.log(Func.version); // 1.0

相似的,还可以扩展枚举型。

enum Color {
  Red,
}
namespace Color {
  export let yellow = "yellow";
  export function func() {}
}
console.log(Color); // {0: "Red", Red: 0, yellow: "yellow", func: ƒ}

另外需要注意,命名空间和类与函数合并时,命名空间必须写在类/函数之后,为什么呢?

编译成 es5 之后不难发现,声明枚举类型时,会提前声明变量,因此不需要提前声明函数;而类/函数不同,都是需要将函数变量传入闭包中,因此函数需要写在前面。

// 类
var Person = /** @class */ (function () {
  function Person() {}
  return Person;
})();
(function (Person) {
  Person.id = 123123;
  var Man = /** @class */ (function () {
    function Man() {}
    return Man;
  })();
  Person.Man = Man;
})(Person || (Person = {}));

//函数
function Func() {}
(function (Func) {
  Func.version = "1.0";
})(Func || (Func = {}));

// 枚举
var Color;
(function (Color) {
  Color[(Color["Red"] = 0)] = "Red";
})(Color || (Color = {}));
(function (Color) {
  Color.yellow = "yellow";
  function func() {}
  Color.func = func;
})(Color || (Color = {}));

结语

在程序中不建议有多处同名声明,最好可以分装到不同模块内。ts 的声明合并特性也是为了照顾旧的设计模式,使得在老代码中也能使用 ts,还发现一些设计缺陷。

TypeScript 工程系列