声明合并

在 TypeScript 中,只要符合一定条件,就可以将多个独立的同名声明合并成一个声明。合并后的声明将同时具有它包含的多个声明的特性。

虽然本节会介绍这个特性,但并不推荐在项目中使用该特性,这会引发代码组织管理上的问题,降低代码的可维护性,读者简单了解该内容即可。

同类型之间的声明合并

在相同类型之间,多个同名枚举、多个同名接口或多个命名空间会合并成单个枚举、单个接口或单个命名空间。

枚举

TypeScript 支持将枚举成员分开声明,由于 TypeScript 拥有声明合并的特性,因此它们将合并为一个枚举。例如,以下代码定义了一个名为 Answer 的枚举,虽然枚举成员 yes 和 no 分别拆开,单独进行了定义,但是最终两个枚举成员 Answer.no 和 Answer.yes 都可以访问。

enum Answer {
    no = 0
}

enum Answer {
    yes = 1
}

let a1: Answer = Answer.yes;
let a2: Answer = Answer.no

接口

当同时声明了多个同名接口时,它们将自动合并为一个接口,并同时拥有所有接口声明中的属性和方法。例如,以下代码声明了两个接口。第一个接口拥有一个 name 属性,第二个接口拥有一个 introduction() 方法,但它们都具有同样的接口名称 Person,因此它们将合并为同一个接口。这个接口拥有所有的属性和方法,它最后定义了一个 Person 类型的变量 person1,并为该变量赋予了一个符合 Person 类型结构的对象。

interface Person {
    name: string
}

interface Person {
    introduction: () => void
}

let person1: Person = {
    name: "Shank",
    introduction: function () {
        console.log(`My name is ${this.name}`);
    }
}

命名空间

命名空间也支持声明合并。例如,以下代码中声明了两个命名空间,第一个命名空间导出了一个变量 a,第二个命名空间导出了一个函数 test(),由于两个命名空间都用了同一个名字 Space1,因此它们将自动合并为一个命名空间。此时可以像一个命名空间那样使用变量 a 和函数 test()。

namespace Space1 {
    export let a: string = "hello";
}

namespace Space1 {
    export function test(text: string) {
        console.log(text);
    }
}

console.log(Space1.a)//输出"hello"
Space1.test("world") //输出"world"

但注意,没有导出的命名空间成员不可以在另一个命名空间中使用。例如,在以下代码中,虽然两个命名空间都叫 Space2,但是在第一个命名空间中声明的变量 b 并没有导出,因此在第二个命名空间中使用变量 b 将会引起编译错误。

namespace Space2 {
    let b: number = 1;
}

namespace Space2 {
    export function printB() {
        //编译错误:找不到名称"b"。ts(2304)
        console.log(b);
    }
}

如果从编译后的 JavaScript 代码来看,原因就比较清晰了。虽然命名空间 Space2 只有一个,但是立即执行函数有两个,而变量 b 只存在于第一个立即执行函数的作用域中,因此无法在第二个立即执行函数中使用。

var Space2;
(function (Space2) {
    var b = 1;
})(Space2 || (Space2 = {}));
(function (Space2) {
    function printB() {
        console.log(b);
    }
    Space2.printB = printB;
})(Space2 || (Space2 = {}));

不同类型之间的声明合并

除同类型的声明合并之外,TypeScript 还支持不同类型之间的声明合并。不同类型之间的声明合并会大幅降低代码的可读性和可维护性,因此在实际项目中切勿使用。读者可以了解这种场景,避免在实际项目中遇到声明合并导致的非预期情况。

TypeScript 中的各个类型对声明合并的支持情况详见表13-1。

image 2024 02 19 14 13 18 640
Figure 1. 表13-1 各个类型对声明合并的支持情况

基于以上情况,在编程时需要特别注意,避免代码的执行不符合预期。下面将列出不同类型的声明合并。

同名类与接口之间的声明合并会导致类增加非预期成员,示例代码如下。

class Point {
    x: number;
}
interface Point {
    y: number;
}

let point: Point = new Point();
point.x = 1;
point.y = 2;

同名枚举和命名空间的声明合并会导致枚举中既有枚举值又有扩展成员,示例代码如下。

enum Color {
    red = 1,
    green = 2,
    blue = 3
}

namespace Color {
    export let yellow = 4;
    export function printName(color: Color) {
        console.log(Color[color]);
    }
}

console.log(Color.yellow);  //输出4
Color.printName(Color.red); //输出red

同名函数和命名空间的声明合并会导致一个函数既可以以函数形式调用,又可以以对象形式访问其成员,示例代码如下。

function Greeting(name: string): void {
    console.log(Greeting.prefix + name + Greeting.suffix);
}

namespace Greeting {
    export let suffix;
    export let prefix;
}

Greeting.prefix = "Hello "
Greeting.suffix = ", Nice to meet you.";
Greeting("Sam"); //输出"Hello Sam, Nice to meet you."