ZetCode

TypeScript 声明合并

最后修改时间:2025年3月3日

TypeScript 中的声明合并允许您将多个同名声明合并为单个实体。此功能在不更改其原始定义的情况下扩展现有类型(如接口或命名空间)特别有用。本教程将通过实际示例探讨声明合并,以展示其强大功能和灵活性。

TypeScript 支持接口、命名空间和类等特定构造的声明合并,但有一些限制。当两个或多个声明在同一作用域内具有相同的名称时,TypeScript 会将它们合并为统一的类型,并组合它们的属性或成员。这是处理第三方库或模块化代码库的关键机制,在这些代码库中,类型定义需要演进或增强。

该过程是自动且基于规则的:对于接口,合并属性;对于命名空间,聚合成员;对于类,合并更受限制但可以通过环境声明来实现。此功能增强了 TypeScript 的可扩展性,允许开发人员根据特定需求调整类型,同时保持类型安全。

合并接口

同名的接口会合并为单个接口,合并它们的属性。

interface_merging.ts
interface User {
    name: string;
}

interface User {
    age: number;
}

const user: User = { name: "Alice", age: 25 };
console.log(user.name); // Output: Alice
console.log(user.age);  // Output: 25

在此示例中,定义了两个同名的 interface User 声明。TypeScript 将它们合并为一个等效于 { name: string; age: number } 的单个接口。属性 nameage 被合并,并且由此产生的 User 类型在创建像 user 这样的对象时需要两者。省略任一属性(例如,{ name: "Alice" })都会触发编译时错误。

输出“Alice”和 25,确认两个属性都可访问。此合并对于增量构建复杂类型或在不修改源的情况下扩展库接口(例如,向基接口添加可选字段)很有用。它展示了 TypeScript 如何灵活地处理接口声明,同时跨合并的定义强制执行类型安全。

添加可选属性

合并的接口可以为现有类型引入可选属性。

optional_properties.ts
interface Product {
    id: number;
    name: string;
}

interface Product {
    price?: number;
}

const product: Product = { id: 1, name: "Laptop" };
console.log(product); // Output: { id: 1, name: "Laptop" }

在这里,Product 接口声明了两次:第一次是必需的 idname,然后是可选的 price,用 ? 标记。TypeScript 将它们合并为一个 Product 类型:{ id: number; name: string; price?: number }product 对象在没有 price 的情况下满足此类型,因为它不是必需的,而包含 price: 100 也将是有效的。

输出 { id: 1, name: "Laptop" },反映了这种灵活性。此模式非常适合以非破坏性方式扩展类型——添加可选属性不会影响依赖于原始声明的现有代码。它通常用于增强 API 响应类型并添加额外的、非强制性字段等场景。

合并方法

接口可以合并方法以及属性,创建一个统一的契约。

method_merging.ts
interface Logger {
    log(message: string): void;
}

interface Logger {
    clear(): void;
}

const logger: Logger = {
    log: (message) => console.log(message),
    clear: () => console.log("Cleared")
};

logger.log("Hello"); // Output: Hello
logger.clear();      // Output: Cleared

Logger 接口声明了两次:第一次有一个 log 方法,第二次有一个 clear 方法。TypeScript 将它们合并为 { log(message: string): void; clear(): void }logger 对象实现了这个组合接口,提供了这两个方法。调用 log("Hello")clear() 可以按预期工作,输出分别为“Hello”和“Cleared”。

此合并允许您增量地扩展功能——例如,在单独的文件或模块中向基接口添加实用方法。它确保实现必须满足所有合并的成员,在支持模块化设计的同时维护类型安全。这在核心和可选功能分开定义的框架中尤其有用。

合并命名空间

同名的命名空间会合并它们的成员,组合变量、函数或嵌套类型。

namespace_merging.ts
namespace Utils {
    export const version = "1.0";
}

namespace Utils {
    export function greet(name: string) {
        return `Hello, ${name}`;
    }
}

console.log(Utils.version);      // Output: 1.0
console.log(Utils.greet("Bob")); // Output: Hello, Bob

两个 namespace Utils 声明合并为一个命名空间,包含 version(字符串)和 greet(函数)。export 关键字使这些成员在命名空间外部可访问。TypeScript 会自动组合它们,因此 Utils 变为 { version: string; greet(name: string): string }。访问 Utils.version 和调用 Utils.greet("Bob") 分别得到“1.0”和“Hello, Bob”。

这对于跨文件组织代码很有价值——例如,一个文件定义常量,另一个文件添加函数,它们在同一个命名空间下合并。它模仿了 JavaScript 中的模块增强,提供了一种在没有冲突的情况下扩展全局或库命名空间的方法,只要成员名称不与不兼容的类型重叠。

合并接口和命名空间

同名的接口和命名空间可以合并,为接口的类型添加静态成员。

interface_namespace_merging.ts
interface Counter {
    count: number;
}

namespace Counter {
    export function create(initial: number): Counter {
        return { count: initial };
    }
}

const c = Counter.create(5);
console.log(c.count); // Output: 5

Counter 接口定义了一个实例类型 { count: number },而 Counter 命名空间添加了一个静态 create 函数。TypeScript 将它们合并,将命名空间的成员作为静态实用程序与接口的名称关联起来。create 函数返回一个符合接口的对象,而 c.count 访问实例属性,输出 5。

这种模式在 Array 等库中很常见,其中类型(数组实例)和静态方法(例如 Array.from)并存。它对于定义与类型相关的工厂函数或实用程序很有用,可以增强接口的生态系统,而无需更改其实例结构。

合并类和接口

类可以与接口合并以扩展它们的实例类型,但不能扩展它们的静态方面。

class_interface_merging.ts
class Person {
    constructor(public name: string) {}
}

interface Person {
    age: number;
}

const p: Person = new Person("Alice");
p.age = 25;
console.log(p.name); // Output: Alice
console.log(p.age);  // Output: 25

Person 类定义了一个带 name 属性的构造函数,而 Person 接口添加了一个 age 属性。TypeScript 将接口合并到类的实例类型中,因此 Person 实例具有 { name: string; age: number }。对象 p 可以在构造函数后设置 age,并且两个属性都可以访问,输出“Alice”和 25。

此合并仅影响实例方面——接口中的静态属性或方法不会合并到类的静态方面。这是一种用附加字段扩展类实例的方法,通常用于将类与库定义的接口进行回溯兼容,或在关注点分离时(例如,核心类与可选功能)。

与环境声明合并

环境声明可以与现有类型合并以扩展第三方代码。

ambient_merging.ts
// Assume this is in a third-party library
declare namespace Settings {
    export const theme: string;
}

// Our extension in a .d.ts file or script
declare namespace Settings {
    export const fontSize: number;
}

console.log(Settings.theme);    // Output: (assumed) dark
console.log(Settings.fontSize); // Output: (assumed) 16

Settings 命名空间最初是环境声明的(例如,在库中)带有 theme。第二个环境声明合并了 fontSize,最终得到 { theme: string; fontSize: number }。由于这是环境的(此处没有运行时实现),因此输出是假设的(例如,“dark”和 16),基于假设的实现。TypeScript 确保了这些合并声明的类型安全。

这是 TypeScript 对外部代码可扩展性的基石。开发人员可以在声明文件中增强全局对象(例如 Window)或库命名空间,而无需修改原始源代码,这对于适配未类型化或部分类型化的 JavaScript 库非常理想。

合并枚举

枚举可以合并以添加新成员,从而扩展其值集。

enum_merging.ts
enum Color {
    Red = 1,
    Green = 2
}

enum Color {
    Blue = 3
}

console.log(Color.Red);  // Output: 1
console.log(Color.Blue); // Output: 3

两个 enum Color 声明合并为一个枚举,包含 RedGreenBlue 成员。TypeScript 合并了它们的值(1、2、3),将它们视为一个枚举。访问 Color.RedColor.Blue 分别得到 1 和 3。这不太常见,但对于跨文件分割枚举定义或逐步扩展一组常量很有用。

与接口不同,枚举合并需要一致的数字或字符串值,并且不能重新定义现有成员的值(例如,第二个声明中的 Red = 4 会导致冲突)。这是一个小众功能,通常用于大型项目或扩展库中预定义的类枚举结构。

合并重载

函数声明可以合并以创建单个实现的重载签名。

overload_merging.ts
function format(value: string): string;
function format(value: number): string;
function format(value: string | number): string {
    return typeof value === "string" ? value.toUpperCase() : value.toString();
}

console.log(format("hello")); // Output: HELLO
console.log(format(42));      // Output: 42

format 函数的两个重载签名与其实现合并。第一个声明允许 string 输入,第二个允许 number 输入,实现接受 string | number,处理这两种情况。TypeScript 合并了这些签名,支持特定类型的调用:format("hello") 返回“HELLO”(大写字符串),而 format(42) 返回“42”(字符串化的数字)。

此合并提供了比单个联合类型签名更简洁的 API,因为它缩小了每次调用的返回类型期望(尽管此处两者都返回 string)。对于具有不同输入行为的函数(如格式化实用程序或库 API)很有用,可以增强调用者的类型检查和 IDE 支持。

最佳实践

来源

TypeScript 声明合并文档

本教程通过实际示例介绍了 TypeScript 声明合并。使用这些技术来有效地扩展和组合类型定义,同时保持类型安全。

作者

我叫 Jan Bodnar,我是一名热情的程序员,拥有丰富的编程经验。我从 2007 年开始撰写编程文章。迄今为止,我已撰写了 1400 多篇文章和 8 本电子书。我在编程教学方面拥有十多年的经验。

列出所有 TypeScript 教程