Skip to content

声明合并

概述

声明合并(Declaration Merging) 是 TypeScript 的一个重要特性,它允许将多个同名的声明自动合并为一个声明。这意味着你可以在不同的地方多次声明同一个接口、命名空间、类或枚举,TypeScript 编译器会自动将它们合并成一个声明。

声明合并机制使得我们可以:

  • 扩展类型定义:为已有的接口、类或命名空间添加新的成员
  • 模块化类型定义:将类型定义分散在多个文件中,然后自动合并
  • 增强第三方库类型:为第三方库的类型定义添加自定义属性或方法

理解声明合并对于编写高质量的 TypeScript 代码和类型定义文件(.d.ts)非常重要。

声明合并的类型

TypeScript 支持以下几种声明合并:

  1. 接口合并:多个同名接口会合并为一个
  2. 命名空间合并:多个同名命名空间会合并为一个
  3. 命名空间与接口合并:命名空间可以与同名接口合并
  4. 命名空间与类合并:命名空间可以与同名类合并
  5. 命名空间与枚举合并:命名空间可以与同名枚举合并
  6. 模块合并:模块可以与命名空间合并

接口合并

接口合并是最常见的声明合并形式。当多个同名接口被声明时,它们会自动合并为一个接口,包含所有声明的成员。

基本语法

typescript
// 第一个接口声明
interface User {
  name: string;
  age: number;
}

// 第二个接口声明(会自动合并到第一个)
interface User {
  email: string;
  phone?: string;
}

// 合并后的 User 接口包含所有属性
const user: User = {
  name: "John",
  age: 30,
  email: "john@example.com",
  phone: "123-456-7890"
};

合并规则

接口合并遵循以下规则:

  1. 属性合并:如果多个接口中有同名属性,它们的类型必须兼容(相同或可兼容)
  2. 方法合并:同名方法会合并为函数重载,后声明的接口中的方法优先级更高
  3. 顺序无关:接口声明的顺序不影响合并结果
typescript
// 示例 1:属性类型必须兼容
interface Config {
  host: string;
  port: number;
}

interface Config {
  host: string;  // ✅ 类型相同,可以合并
  // port: string;  // ❌ 错误:类型不兼容
  timeout: number;  // ✅ 新属性,可以添加
}

// 示例 2:方法合并为函数重载
interface Calculator {
  add(a: number, b: number): number;
}

interface Calculator {
  add(a: string, b: string): string;  // 函数重载
  subtract(a: number, b: number): number;
}

// 合并后的 Calculator 有两个 add 重载
const calc: Calculator = {
  add(a: number | string, b: number | string): number | string {
    if (typeof a === 'number' && typeof b === 'number') {
      return a + b;
    }
    return String(a) + String(b);
  },
  subtract(a: number, b: number): number {
    return a - b;
  }
};

实际应用场景

接口合并常用于扩展类型定义,特别是在为第三方库添加类型或创建可扩展的配置接口时:

typescript
// 场景 1:扩展第三方库的类型
// 假设这是第三方库的类型定义
interface Window {
  location: Location;
}

// 我们可以在自己的代码中扩展 Window 接口
interface Window {
  myCustomProperty: string;
  myCustomMethod(): void;
}

// 现在 Window 类型包含了所有属性
declare const window: Window;
window.myCustomProperty;  // ✅ 可以使用
window.myCustomMethod();   // ✅ 可以使用
typescript
// 场景 2:模块化配置接口
// config/base.ts
interface AppConfig {
  appName: string;
  version: string;
}

// config/database.ts
interface AppConfig {
  database: {
    host: string;
    port: number;
  };
}

// config/api.ts
interface AppConfig {
  api: {
    baseUrl: string;
    timeout: number;
  };
}

// 所有配置合并为一个接口
const config: AppConfig = {
  appName: "My App",
  version: "1.0.0",
  database: {
    host: "localhost",
    port: 5432
  },
  api: {
    baseUrl: "https://api.example.com",
    timeout: 5000
  }
};

命名空间合并

命名空间也支持声明合并,多个同名命名空间会自动合并为一个。

基本语法

typescript
// 第一个命名空间声明
namespace Utils {
  export function formatDate(date: Date): string {
    return date.toISOString();
  }
}

// 第二个命名空间声明(会自动合并)
namespace Utils {
  export function formatNumber(num: number): string {
    return num.toFixed(2);
  }
  
  export const VERSION = '1.0.0';
}

// 合并后可以访问所有成员
Utils.formatDate(new Date());
Utils.formatNumber(3.14159);
console.log(Utils.VERSION);

命名空间嵌套合并

嵌套的命名空间也可以合并:

typescript
namespace MyLib {
  export namespace API {
    export function get(url: string): Promise<any> {
      return fetch(url).then(r => r.json());
    }
  }
}

namespace MyLib {
  export namespace API {
    export function post(url: string, data: any): Promise<any> {
      return fetch(url, {
        method: 'POST',
        body: JSON.stringify(data)
      }).then(r => r.json());
    }
  }
  
  export namespace Utils {
    export function capitalize(str: string): string {
      return str.charAt(0).toUpperCase() + str.slice(1);
    }
  }
}

// 使用合并后的命名空间
MyLib.API.get('/users');
MyLib.API.post('/users', { name: 'John' });
MyLib.Utils.capitalize('hello');

命名空间与接口合并

命名空间可以与同名接口合并,这允许我们为接口添加静态成员。

基本语法

typescript
// 定义接口
interface User {
  name: string;
  age: number;
}

// 为接口添加静态成员(通过命名空间)
namespace User {
  export function create(name: string, age: number): User {
    return { name, age };
  }
  
  export function validate(user: User): boolean {
    return user.name.length > 0 && user.age >= 0;
  }
  
  export const MIN_AGE = 0;
  export const MAX_AGE = 150;
}

// 使用接口实例和静态方法
const user: User = User.create('Alice', 25);  // 使用静态方法创建
const isValid = User.validate(user);           // 使用静态方法验证
console.log(User.MAX_AGE);                      // 访问静态常量

实际应用场景

这种模式常用于创建既有实例方法又有静态方法的类型:

typescript
// 定义事件接口
interface Event {
  type: string;
  timestamp: number;
  data: any;
}

// 为事件接口添加静态工厂方法
namespace Event {
  export function create(type: string, data: any): Event {
    return {
      type,
      timestamp: Date.now(),
      data
    };
  }
  
  export function isEvent(obj: any): obj is Event {
    return obj && 
           typeof obj.type === 'string' && 
           typeof obj.timestamp === 'number';
  }
  
  export const TYPES = {
    CLICK: 'click',
    SUBMIT: 'submit',
    LOAD: 'load'
  } as const;
}

// 使用
const clickEvent = Event.create(Event.TYPES.CLICK, { x: 100, y: 200 });
const isValid = Event.isEvent(clickEvent);  // true

命名空间与类合并

命名空间可以与同名类合并,为类添加静态成员。

基本语法

typescript
// 定义类
class User {
  constructor(public name: string, public age: number) {}
  
  greet(): string {
    return `Hello, I'm ${this.name}`;
  }
}

// 为类添加静态成员(通过命名空间)
namespace User {
  export function create(name: string, age: number): User {
    return new User(name, age);
  }
  
  export function fromJSON(json: string): User {
    const data = JSON.parse(json);
    return new User(data.name, data.age);
  }
  
  export const DEFAULT_AGE = 18;
}

// 使用实例方法和静态方法
const user1 = new User('Bob', 30);
const user2 = User.create('Alice', 25);        // 使用静态方法
const user3 = User.fromJSON('{"name":"Charlie","age":35}');
console.log(user1.greet());                    // "Hello, I'm Bob"
console.log(User.DEFAULT_AGE);                 // 18

提示

虽然可以通过命名空间为类添加静态成员,但在现代 TypeScript 中,更推荐直接在类中定义静态成员:

typescript
class User {
  static DEFAULT_AGE = 18;
  
  static create(name: string, age: number): User {
    return new User(name, age);
  }
  
  constructor(public name: string, public age: number) {}
}

命名空间与枚举合并

命名空间可以与同名枚举合并,为枚举添加方法和属性。

基本语法

typescript
// 定义枚举
enum Status {
  Pending = 'pending',
  Active = 'active',
  Inactive = 'inactive'
}

// 为枚举添加工具方法(通过命名空间)
namespace Status {
  export function isValid(value: string): value is Status {
    return Object.values(Status).includes(value as Status);
  }
  
  export function getLabel(status: Status): string {
    const labels: Record<Status, string> = {
      [Status.Pending]: '等待中',
      [Status.Active]: '活跃',
      [Status.Inactive]: '非活跃'
    };
    return labels[status];
  }
  
  export function getNext(status: Status): Status | null {
    const order = [Status.Pending, Status.Active, Status.Inactive];
    const currentIndex = order.indexOf(status);
    return currentIndex < order.length - 1 ? order[currentIndex + 1] : null;
  }
}

// 使用枚举值和命名空间方法
const status = Status.Active;
const label = Status.getLabel(status);              // "活跃"
const isValid = Status.isValid('active');            // true
const nextStatus = Status.getNext(status);           // Status.Inactive

实际应用场景

这种模式常用于为枚举添加业务逻辑方法:

typescript
enum HttpStatus {
  OK = 200,
  BadRequest = 400,
  Unauthorized = 401,
  NotFound = 404,
  InternalServerError = 500
}

namespace HttpStatus {
  export function isSuccess(status: HttpStatus): boolean {
    return status >= 200 && status < 300;
  }
  
  export function isError(status: HttpStatus): boolean {
    return status >= 400;
  }
  
  export function getMessage(status: HttpStatus): string {
    const messages: Record<HttpStatus, string> = {
      [HttpStatus.OK]: '请求成功',
      [HttpStatus.BadRequest]: '请求错误',
      [HttpStatus.Unauthorized]: '未授权',
      [HttpStatus.NotFound]: '未找到',
      [HttpStatus.InternalServerError]: '服务器错误'
    };
    return messages[status];
  }
}

// 使用
const response = HttpStatus.OK;
if (HttpStatus.isSuccess(response)) {
  console.log(HttpStatus.getMessage(response));  // "请求成功"
}

模块合并

模块可以与命名空间合并,这在声明文件(.d.ts)中特别有用。

基本语法

typescript
// 模块声明
declare module 'my-module' {
  export interface Config {
    apiUrl: string;
  }
  
  export function initialize(config: Config): void;
}

// 扩展模块(通过命名空间合并)
declare module 'my-module' {
  export namespace Utils {
    export function formatDate(date: Date): string;
    export function formatNumber(num: number): string;
  }
}

// 使用扩展后的模块
import { Config, initialize, Utils } from 'my-module';

const config: Config = { apiUrl: 'https://api.example.com' };
initialize(config);
Utils.formatDate(new Date());

实际应用场景

模块合并常用于为第三方库添加类型定义:

typescript
// 为 jQuery 添加类型定义
declare namespace jQuery {
  interface AjaxSettings {
    url: string;
    method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
    data?: any;
    success?: (data: any) => void;
    error?: (error: any) => void;
  }
  
  function ajax(settings: AjaxSettings): void;
  
  namespace fn {
    function extend(obj: any): void;
  }
}

// 扩展 jQuery 类型
declare namespace jQuery {
  namespace ui {
    interface DialogOptions {
      title?: string;
      modal?: boolean;
      width?: number;
    }
    
    function dialog(options: DialogOptions): void;
  }
}

// 使用
jQuery.ajax({
  url: '/api/users',
  method: 'GET',
  success: (data) => console.log(data)
});

jQuery.ui.dialog({
  title: 'My Dialog',
  modal: true
});

合并规则和限制

合并规则总结

  1. 接口合并

    • 同名属性类型必须兼容
    • 同名方法会合并为函数重载
    • 后声明的接口中的方法优先级更高
  2. 命名空间合并

    • 导出的成员会合并
    • 未导出的成员不会合并
    • 嵌套命名空间也会合并
  3. 命名空间与其他声明合并

    • 命名空间必须与接口/类/枚举同名
    • 命名空间中的导出成员会成为静态成员

合并限制

typescript
// ❌ 错误:类型别名不支持合并
type User = {
  name: string;
};

// type User = {  // Error: Duplicate identifier 'User'
//   age: number;
// };

// ✅ 正确:使用接口代替
interface User {
  name: string;
}

interface User {  // ✅ 可以合并
  age: number;
}

// ❌ 错误:属性类型不兼容
interface Config {
  port: number;
}

interface Config {
  port: string;  // Error: Subsequent property declarations must have the same type
}

// ❌ 错误:命名空间与类合并时,命名空间必须在类之后声明
namespace MyClass {
  export function helper(): void {}
}

class MyClass {  // Error: Class cannot be declared after namespace with same name
  method(): void {}
}

使用示例

示例 1:扩展全局类型

typescript
// 扩展全局 Window 对象
interface Window {
  myApp: {
    version: string;
    config: {
      apiUrl: string;
    };
  };
}

// 使用
window.myApp = {
  version: '1.0.0',
  config: {
    apiUrl: 'https://api.example.com'
  }
};

示例 2:创建可扩展的配置系统

typescript
// base-config.ts
interface AppConfig {
  appName: string;
  environment: 'development' | 'production';
}

// database-config.ts
interface AppConfig {
  database: {
    host: string;
    port: number;
    name: string;
  };
}

// api-config.ts
interface AppConfig {
  api: {
    baseUrl: string;
    timeout: number;
    retries: number;
  };
}

// 所有配置自动合并
const config: AppConfig = {
  appName: 'My App',
  environment: 'production',
  database: {
    host: 'localhost',
    port: 5432,
    name: 'mydb'
  },
  api: {
    baseUrl: 'https://api.example.com',
    timeout: 5000,
    retries: 3
  }
};

示例 3:为枚举添加工具方法

typescript
enum LogLevel {
  Debug = 0,
  Info = 1,
  Warning = 2,
  Error = 3
}

namespace LogLevel {
  export function fromString(level: string): LogLevel | null {
    const levelMap: Record<string, LogLevel> = {
      'debug': LogLevel.Debug,
      'info': LogLevel.Info,
      'warning': LogLevel.Warning,
      'error': LogLevel.Error
    };
    return levelMap[level.toLowerCase()] || null;
  }
  
  export function toString(level: LogLevel): string {
    const names: Record<LogLevel, string> = {
      [LogLevel.Debug]: 'DEBUG',
      [LogLevel.Info]: 'INFO',
      [LogLevel.Warning]: 'WARNING',
      [LogLevel.Error]: 'ERROR'
    };
    return names[level];
  }
  
  export function isHigherOrEqual(level: LogLevel, threshold: LogLevel): boolean {
    return level >= threshold;
  }
}

// 使用
const level = LogLevel.fromString('info');  // LogLevel.Info
if (level !== null) {
  console.log(LogLevel.toString(level));     // "INFO"
  const shouldLog = LogLevel.isHigherOrEqual(level, LogLevel.Warning);  // false
}

注意事项

注意

  1. 类型别名不支持合并:只有接口、命名空间、类、枚举和模块支持声明合并,类型别名(type)不支持。

  2. 属性类型必须兼容:合并接口时,同名属性的类型必须完全相同或兼容,否则会报错。

  3. 函数重载顺序:接口合并时,同名方法会合并为函数重载,后声明的接口中的方法签名优先级更高。

  4. 命名空间位置:命名空间与类合并时,命名空间必须在类之后声明。

  5. 避免过度使用:虽然声明合并很强大,但过度使用会使代码难以理解和维护。在现代项目中,优先考虑使用接口继承或组合。

最佳实践

  1. 优先使用接口继承:对于需要扩展类型的情况,优先考虑使用 extends 关键字而不是声明合并。

  2. 明确文档化:如果使用声明合并,应该在文档中明确说明,帮助其他开发者理解代码结构。

  3. 用于类型定义文件:声明合并最适合用于声明文件(.d.ts),特别是为第三方库添加类型定义。

  4. 保持一致性:在同一个项目中,保持声明合并使用方式的一致性。

  5. 考虑可维护性:虽然声明合并很强大,但要考虑代码的可维护性,避免创建过于复杂的合并结构。

相关链接

基于 VitePress 构建