跳至主要內容

声明合并

樱桃茶大约 15 分钟

声明合并

声明合并是 TypeScript 特有的一个概念,它指的是编译器如何处理多个同名的声明。在 TypeScript 中,某些类型的声明可以自动合并。了解这个特性非常重要,因为它会影响到你如何组织代码以及类型怎样被推断。

下面是几种声明合并的情形:

  • 接口合并

    • 当你声明了两个同名的接口时,它们会合并成一个单独的接口。
    • 这个合并的接口拥有两个原始接口的所有成员。
    • 如果成员具有相同的名字但类型不兼容,编译器会报错。
    interface Box {
      height: number;
      width: number;
    }
    
    interface Box {
      scale: number;
    }
    
    let box: Box = { height: 5, width: 6, scale: 10 };
    

    在这个例子中,Box 接口被声明了两次,但实际上 TypeScript 会把它们合并成一个接口,所以 box 变量包含了三个属性。

  • 命名空间与类、函数或枚举的合并

    • 当一个命名空间和其他类型声明(类、函数、枚举)拥有相同的名字时,它们也会发生合并。
    • 对于函数和命名空间的合并,你可以给函数添加静态属性。
    • 类和命名空间合并可以用来扩展类的静态属性和方法。
    function count(start: number): void {
      console.log(start);
    }
    namespace count {
      export let interval = 1000;
    }
    
    count(10); // 输出:10
    console.log(count.interval); // 输出:1000
    

    在这个例子中,我们定义了一个函数 count 和一个同名的命名空间,它们被合并了。所以 count 函数现在有了一个 interval 静态属性。

  • 枚举的合并

    • 枚举也可以与同名的命名空间进行合并。
    • 这使得你可以向枚举添加静态属性或方法。
    enum Colors {
      Red,
      Green,
      Blue,
    }
    namespace Colors {
      export function mixColor(colorName: string) {
        if (colorName == "Yellow") {
          return Colors.Green + Colors.Red;
        } else if (colorName == "White") {
          return Colors.Red + Colors.Green + Colors.Blue;
        } else if (colorName == "Magenta") {
          return Colors.Red + Colors.Blue;
        }
        return 0;
      }
    }
    
    console.log(Colors.mixColor("Yellow")); // 输出:1 (因为Green是0,Red是1)
    

    在这个例子中,Colors 枚举与同名的命名空间合并,通过命名空间我们为 Colors 枚举添加了一个新方法 mixColor

记住,不是所有的声明都可以合并。只有某些特定的 TypeScript 声明才可以,比如接口、命名空间、类和枚举。此外,声明合并是根据名称进行的,所以在应用中使用合理的、描述性强的命名可以帮助避免意外的合并。

基础概念

声明合并是 TypeScript 的一个特性,它指的是编译器会将两个同名的声明合并成一个。这其中涉及到接口和命名空间的合并。

  • 接口合并:
    • 当你声明两个同名的接口时,TypeScript 会将它们的成员合并到一个接口中。
    • 这意味着你可以将一个接口的定义分散在不同的地方,但它们最终会被视为单个接口。
    • 合并处理规则如下:
      • 对于非函数的成员,如果同名成员是唯一的,则合并后的接口将拥有这个成员。
      • 对于函数成员,每个同名函数声明都会被合并到一个重载函数列表中。
interface Box {
  height: number;
  width: number;
}

interface Box {
  scale: number;
}

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

在上面的例子中,Box接口被声明了两次,并且都会被合并成一个接口,所以变量box包含了三个属性:heightwidthscale

  • 命名空间合并:
    • 类似于接口,同名的命名空间也会进行合并。
    • 命名空间的合并会把两个命名空间的导出成员合并到一个命名空间里。
    • 如果命名空间中出现了同名的非导出成员,则会产生冲突并且编译器会报错。
namespace Animals {
  export class Zebra {}
}

namespace Animals {
  export interface Legged {
    numberOfLegs: number;
  }
  export class Dog {}
}

// 合并后的Animals命名空间包含Zebra和Dog类以及Legged接口。

这个例子展示了如何通过声明合并来组织代码,使得相同逻辑或者领域相关的类型和值可以放在同一个命名空间内。

  • 模块扩展:
    • 通过声明与模块同名的命名空间,可以扩展模块的静态能力。
    • 当模块和命名空间具有相同的名字时,所有导出的接口或类都会暴露在这个命名空间中。
module Animals {
  export interface Legged {
    numberOfLegs: number;
  }
}

namespace Animals {
  export function walk(animal: Legged) {
    console.log(`Walking on ${animal.numberOfLegs} legs`);
  }
}

// 因为命名空间Animals扩展了模块Animals,我们可以使用walk函数。

注意,虽然声明合并允许你在不同地方定义同名的接口、命名空间或模块,但使用这种特性需要谨慎,因为它可能导致命名冲突、可读性降低以及其他结构上的问题。通常建议尽可能保持代码简洁明了,避免不必要的复杂性。

合并接口

  • TypeScript 的声明合并是指编译器将两个同名的声明合并为一个单一定义。这个合并后的定义拥有原始两个声明的特性。这种机制对于接口(Interfaces)尤其常见。

  • 接口合并的基本规则:

    • 如果两个接口中的成员不冲突(即,它们没有相同名称的成员或者相同名称的成员类型是兼容的),那么这两个接口可以无缝合并。
    • 对于函数成员,每个同名函数声明都会被视为这个函数的一个重载。
    • 对于非函数成员,只要成员名字相同但类型不兼容,则会报错。
  • 示例 1:合并普通属性和方法

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

    这里,Box 接口被合并,形成了一个包含 heightwidthscale 属性的新接口。

  • 示例 2:合并函数成员

    interface Cloner {
      clone(animal: Animal): Animal;
    }
    
    interface Cloner {
      clone(animal: Sheep): Sheep;
    }
    
    // 合并后,Cloner 接口具有重载的 clone 方法:
    interface Cloner {
      clone(animal: Animal): Animal;
      clone(animal: Sheep): Sheep;
    }
    

    注意,在实际 TypeScript 编码中,你应该直接写出带有重载的接口,上述分开写法主要用于示意声明合并过程。

  • 示例 3:合并具有冲突类型的成员

    interface Document {
      createElement(tagName: any): Element;
    }
    
    interface Document {
      createElement(tagName: string): HTMLElement;
    }
    
    interface Document {
      createElement(tagName: "div"): HTMLDivElement;
      createElement(tagName: "span"): HTMLSpanElement;
    }
    
    // 结果 Document 接口被合并,包含所有 createElement 的重载版本
    

    在这个例子中,即使后来的 createElement 方法重载看似与先前的冲突,实际上它们是按照 TypeScript 的函数重载规则来处理的,最具体的重载应该放在最上面。

通过理解声明合并,可以更灵活地组织 TypeScript 代码,尤其是在做类型扩展或者修饰现有类型声明时。这也体现了 TypeScript 设计的灵活性和强大之处,能够适应复杂多变的开发需求。

合并命名空间

合并命名空间(Namespace Merging)是 TypeScript 中一个非常实用的特性,它允许开发者将多个同名的命名空间合并为一个。这主要用于组织和管理代码,尤其是在大型项目中或者在逐步扩展类型定义时非常有用。以下是关于命名空间合并的几个要点和例子:

  • 同名命名空间的合并:当你声明了两个相同名称的命名空间时,它们会自动合并。这意味着第二个命名空间内的成员被加到第一个命名空间内,就像它们原本就在那里一样。
namespace Animals {
  export class Zebra {}
}

namespace Animals {
  export interface Legged {
    numberOfLegs: number;
  }
  export class Dog {}
}

// 使用合并后的Animals命名空间
let zebra = new Animals.Zebra();
let dog = new Animals.Dog();
  • 命名空间与类的合并:如果你有一个类和一个同名的命名空间,它们也可以合并。命名空间的成员看起来就像是类的静态成员。
class Album {
  label: Album.AlbumLabel;
}
namespace Album {
  export class AlbumLabel {}
}

let album = new Album();
album.label = new Album.AlbumLabel();
  • 命名空间与接口的合并:就像命名空间能够与类合并一样,它们也可以与接口合并。这使得你能够向已存在的接口添加新的属性或方法。
interface Cloner {
  clone(animal: Animal): Animal;
}

namespace Cloner {
  export function clone(animal: Sheep): Sheep {
    // 返回一个Sheep实例的复制体
    return { ...animal };
  }
}

interface Animal {
  name: string;
}

interface Sheep extends Animal {
  wool: boolean;
}
  • 导出和非导出成员的合并:只有导出(export)的成员会被合并到命名空间外部。如果你有一个非导出成员,它将仅在其原始的命名空间内可见。
namespace Box {
  export interface OpenBox {
    isOpen: boolean;
  }
  let contents: string;
  export function open() {
    contents = "opened";
  }
}

// contents 不可访问,因为它没有被导出
// Box.contents; // Error: Property 'contents' does not exist on type 'typeof Box'
Box.open(); // 正确

通过上述例子和解释,可以看出命名空间的合并提供了一种灵活的方式来组织代码,允许增量地构建类型定义或功能,非常适合处理大型项目或库的开发。

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

声明合并是 TypeScript 特有的一个特性,指的是编译器将两个同名的声明合并为一个单一定义。这个合并后的定义拥有原先两个声明的特性。当合并命名空间与类、函数和枚举类型时,要遵循一定的规则和方式。下面分别解释这些合并情况:

  • 命名空间与类合并

    • 当命名空间与一个类合并时,命名空间的内容会被视为类的静态属性或方法。
    • 这让我们可以组织代码,把相关的类和辅助功能放在一起。
    class Album {
      label: Album.AlbumLabel;
    }
    namespace Album {
      export class AlbumLabel {}
    }
    
    let album = new Album();
    album.label = new Album.AlbumLabel();
    

    在以上例子中,AlbumLabel类作为Album类的静态成员,通过Album.AlbumLabel来访问。

  • 命名空间与函数合并

    • 函数无法携带属性,但如果你使用命名空间与函数合并,那么命名空间有效地为函数增加了属性。
    function count(): void {
      count.hits += 1;
    }
    namespace count {
      export let hits: number = 0;
    }
    
    count();
    console.log(count.hits); // 输出:1
    

    这里count函数通过与命名空间合并获得了hits属性,之后调用count()时可以累计次数。

  • 命名空间与枚举类型合并

    • 当命名空间与枚举类型合并时,命名空间的导出会被添加到枚举类型的值上。
    enum Color {
      Red = 1,
      Green = 2,
      Blue = 4,
    }
    namespace Color {
      export function mixColor(colorName: string): number {
        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;
        }
        // 更多颜色混合逻辑...
        return 0;
      }
    }
    
    Color.mixColor("yellow"); // 返回:3
    

    在这个例子中,我们给Color枚举增加了一个mixColor函数,它能够根据提供的颜色名称返回混合颜色的值。

以上就是 TypeScript 中声明合并中关于命名空间与类、函数和枚举类型合并的解释及示例。通过这种方式,TypeScript 允许开发者在保持代码模块化的同时,扩展现有的类型和函数。

合并命名空间和类

声明合并(Declaration Merging)是 TypeScript 的一个独特特性,它允许我们将多个相同名称的声明合并为一个定义。这通常会在使用命名空间或者类时遇到。

当你在 TypeScript 中合并命名空间和类时,主要是为了扩展类的功能。我们可以通过这种方式为类添加额外的属性或方法。类与命名空间合并的规则如下:

  • 类定义必须在合并前声明。
  • 命名空间必须跟类具有相同的名字。

下面通过示例来说明这个概念:

class Album {
  label: Album.AlbumLabel;
}
namespace Album {
  export class AlbumLabel {}
}

let album = new Album();
album.label = new Album.AlbumLabel();

在上面的例子中:

  • 我们首先创建了一个Album类,其中包含一个名为label的属性,该属性是Album.AlbumLabel类型的。
  • 然后我们定义了一个与Album类同名的命名空间,命名空间内部定义了一个AlbumLabel类。
  • TypeScript 允许这两种声明合并,现在Album类就拥有了AlbumLabel这个嵌套类。
  • 当我们创建Album的实例时,我们可以实例化一个Album.AlbumLabel作为其label属性。

通过声明合并,我们可以组织代码,使得相关的功能和补充的定义都放在一起,而不是分散在不同的地方。这样做能够让代码的结构更加清晰,易于管理。

非法的合并

在 TypeScript 中,声明合并是指编译器将两个同名的声明合并为一个单一定义。这个行为允许我们以分散的方式书写代码,然后将它们合并为一个完整的类型。有些类型的声明可以安全地合并,但有些则不行,后者就是被认为是“非法”的合并。

以下是一些关于非法合并的例子:

  • 当你尝试将一个枚举类型与其它类型进行合并时,这是不允许的:

    enum Box {
      Small,
      Medium,
      Large,
    }
    // 非法合并:不能将类与枚举合并。
    class Box {
      static Small = "small";
    }
    
  • 同样,你不能把命名空间和其它非函数类型合并:

    namespace Box {
      export let size = "small";
    }
    // 非法合并:不能将变量与命名空间合并。
    var Box;
    
  • 尝试将两个非函数的接口或两个类合并到一起,但它们有不兼容的成员(例如,同名的成员却拥有不同类型的属性),这也是非法的合并:

    interface Point {
      x: number;
    }
    interface Point {
      // 非法合并:'x' 的类型不兼容。
      x: string;
    }
    
    class Point {
      x: number;
    }
    class Point {
      // 非法合并:不能有多个类声明。
      x: string;
    }
    

记住,当你发现自己在合并声明时遇到报错,很可能是因为你尝试了一个 TypeScript 认为“非法”的合并操作。确保声明的同名成员兼容,或者重新考虑代码设计,避免需要合并的情况。

模块扩展

模块扩展 (Module Augmentation) 是 TypeScript 中的一种特性,允许你扩展已存在的模块的类型信息。当你希望在现有的模块中添加新的属性、方法或属性到已经存在的类型时,就可以使用模块扩展。

  • 假设有一个模块 my-module.ts 导出了一个接口 SomeInterface

    // my-module.ts
    export interface SomeInterface {
      a: number;
    }
    
  • 如果要扩展这个接口(比如添加一个新的属性 b),你不能直接修改原始的模块文件。相反,可以创建一个新的声明文件,例如 my-module-extension.d.ts

    // my-module-extension.d.ts
    import "my-module";
    
    declare module "my-module" {
      interface SomeInterface {
        b: string; // 新增属性
      }
    }
    
  • 当你在另一个文件中导入 my-module 并使用 SomeInterface 接口时,你会发现 SomeInterface 现在既有原有的属性 a 又有新增的属性 b

    // 使用扩展后的SomeInterface
    import { SomeInterface } from "my-module";
    
    const example: SomeInterface = {
      a: 10,
      b: "hello",
    };
    
  • 模块扩展不仅仅限于接口。你也可以扩展模块中的类、函数或枚举等。

  • 需要注意的是,为了使模块扩展生效,你需要确保扩展声明文件被包含在编译上下文中,通常通过在 tsconfig.json 文件中的 "include""files" 属性中引用它们,或者在项目中正确放置 .d.ts 文件以被自动包括。

模块扩展提供了一种强大的方式来增加现有模块的可用性,而不需要更改原始源代码,非常适合当你使用第三方库但需要额外的类型定义时。

全局扩展

声明合并是 TypeScript 中的一个特性,它允许我们将两个同名的声明合并成一个。这通常用于扩展现有的类型、接口或模块。

模块扩展

当你想要在某个模块中添加新的属性或方法时,你可以通过声明合并来实现模块扩展。

  • 假设我们有一个模块myModule,里面有一个函数foo

    // myModule.ts
    export function foo() {
      console.log("foo");
    }
    
  • 现在我们想要在不修改原始模块的情况下添加一个新函数barmyModule模块。

  • 我们可以创建一个新的文件,并重新声明myModule模块,然后添加我们的新函数。

    // additional.ts
    import * as myModule from "./myModule";
    
    declare module "./myModule" {
      export function bar(): void;
    }
    
    myModule.bar = function () {
      console.log("bar");
    };
    
  • 在上述代码中,declare module './myModule' {...}告诉 TypeScript 我们正在扩展已有的myModule模块。

  • 这样,当你引入myModule时,你会同时拥有foobar两个函数。

全局扩展

全局扩展是指当你想要向全局作用域添加类型、值等时使用的一种声明合并。

  • 当你想要添加全局变量或者全局类型时可以使用全局扩展。

  • 例如,假设我们想要在全局作用域中添加一个全局函数greet

    // global.d.ts
    declare global {
      function greet(msg: string): void;
    }
    
    window.greet = function (msg) {
      console.log(msg);
    };
    
  • 在上例中,declare global {...}告诉 TypeScript 我们正在添加一个新的全局函数。

  • 之后,在任何模块中,你都可以直接调用greet,无需任何导入。

请记住,使用声明合并需要谨慎,因为它可能导致名称空间的冲突,尤其是在全局范围内。确保扩展的内容能够清晰地指明来源,以避免潜在的混淆。