声明合并
概述
声明合并(Declaration Merging) 是 TypeScript 的一个重要特性,它允许将多个同名的声明自动合并为一个声明。这意味着你可以在不同的地方多次声明同一个接口、命名空间、类或枚举,TypeScript 编译器会自动将它们合并成一个声明。
声明合并机制使得我们可以:
- 扩展类型定义:为已有的接口、类或命名空间添加新的成员
- 模块化类型定义:将类型定义分散在多个文件中,然后自动合并
- 增强第三方库类型:为第三方库的类型定义添加自定义属性或方法
理解声明合并对于编写高质量的 TypeScript 代码和类型定义文件(.d.ts)非常重要。
声明合并的类型
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:属性类型必须兼容
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;
}
};实际应用场景
接口合并常用于扩展类型定义,特别是在为第三方库添加类型或创建可扩展的配置接口时:
// 场景 1:扩展第三方库的类型
// 假设这是第三方库的类型定义
interface Window {
location: Location;
}
// 我们可以在自己的代码中扩展 Window 接口
interface Window {
myCustomProperty: string;
myCustomMethod(): void;
}
// 现在 Window 类型包含了所有属性
declare const window: Window;
window.myCustomProperty; // ✅ 可以使用
window.myCustomMethod(); // ✅ 可以使用// 场景 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
}
};命名空间合并
命名空间也支持声明合并,多个同名命名空间会自动合并为一个。
基本语法
// 第一个命名空间声明
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);命名空间嵌套合并
嵌套的命名空间也可以合并:
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');命名空间与接口合并
命名空间可以与同名接口合并,这允许我们为接口添加静态成员。
基本语法
// 定义接口
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); // 访问静态常量实际应用场景
这种模式常用于创建既有实例方法又有静态方法的类型:
// 定义事件接口
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命名空间与类合并
命名空间可以与同名类合并,为类添加静态成员。
基本语法
// 定义类
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 中,更推荐直接在类中定义静态成员:
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) {}
}命名空间与枚举合并
命名空间可以与同名枚举合并,为枚举添加方法和属性。
基本语法
// 定义枚举
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实际应用场景
这种模式常用于为枚举添加业务逻辑方法:
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)中特别有用。
基本语法
// 模块声明
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());实际应用场景
模块合并常用于为第三方库添加类型定义:
// 为 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
});合并规则和限制
合并规则总结
接口合并:
- 同名属性类型必须兼容
- 同名方法会合并为函数重载
- 后声明的接口中的方法优先级更高
命名空间合并:
- 导出的成员会合并
- 未导出的成员不会合并
- 嵌套命名空间也会合并
命名空间与其他声明合并:
- 命名空间必须与接口/类/枚举同名
- 命名空间中的导出成员会成为静态成员
合并限制
// ❌ 错误:类型别名不支持合并
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:扩展全局类型
// 扩展全局 Window 对象
interface Window {
myApp: {
version: string;
config: {
apiUrl: string;
};
};
}
// 使用
window.myApp = {
version: '1.0.0',
config: {
apiUrl: 'https://api.example.com'
}
};示例 2:创建可扩展的配置系统
// 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:为枚举添加工具方法
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
}注意事项
注意
类型别名不支持合并:只有接口、命名空间、类、枚举和模块支持声明合并,类型别名(
type)不支持。属性类型必须兼容:合并接口时,同名属性的类型必须完全相同或兼容,否则会报错。
函数重载顺序:接口合并时,同名方法会合并为函数重载,后声明的接口中的方法签名优先级更高。
命名空间位置:命名空间与类合并时,命名空间必须在类之后声明。
避免过度使用:虽然声明合并很强大,但过度使用会使代码难以理解和维护。在现代项目中,优先考虑使用接口继承或组合。
最佳实践
优先使用接口继承:对于需要扩展类型的情况,优先考虑使用
extends关键字而不是声明合并。明确文档化:如果使用声明合并,应该在文档中明确说明,帮助其他开发者理解代码结构。
用于类型定义文件:声明合并最适合用于声明文件(
.d.ts),特别是为第三方库添加类型定义。保持一致性:在同一个项目中,保持声明合并使用方式的一致性。
考虑可维护性:虽然声明合并很强大,但要考虑代码的可维护性,避免创建过于复杂的合并结构。
相关链接
- 接口 - 了解接口的基本用法
- 类型别名 - 了解类型别名与接口的区别
- 命名空间 - 深入了解命名空间的使用
- 模块系统概述 - 了解模块系统的基本概念
- TypeScript 官方文档 - 声明合并