目录
第九章 声明合并
声明合并(Declaration Merging)是TypeScript的一个独特特性,它允许编译器将多个同名的声明合并为一个单一的声明。这种机制使得可以分多次定义同一个实体,特别适用于扩展第三方库类型定义或组织大型代码库。
9.1 基本概念
在TypeScript中,“声明”会创建实体,包括命名空间、类型或值三种之一。声明合并就是将具有相同名称的多个独立声明合并为一个定义。
// 接口合并
interface Box {
height: number;
width: number;
}
interface Box {
scale: number;
}
// 合并后的Box接口
let box: Box = { height: 5, width: 6, scale: 10 };
9.2 可合并的声明类型
9.2.1 接口合并
接口是合并最常见和最有用的场景。多个同名接口会自动合并。
// 接口合并规则:
// 1. 非函数成员必须唯一,否则报错
// 2. 同名函数成员会重载
interface Document {
createElement(tagName: "div"): HTMLDivElement;
createElement(tagName: "span"): HTMLSpanElement;
}
interface Document {
createElement(tagName: string): HTMLElement;
createElement(tagName: any): Element;
}
// 合并后的Document接口包含所有createElement重载
合并规则详解: - 非函数成员:必须是唯一的类型,如果出现重复且类型不兼容会报错 - 函数成员:每个同名函数声明都会被当作重载处理 - 重载顺序:后面的接口声明会排在前面,但具体实现签名始终在最后
interface Animal {
name: string;
move(): void;
}
interface Animal {
age: number;
move(): string; // 重载
}
// 合并结果:
// interface Animal {
// name: string;
// age: number;
// move(): string; // 重载1
// move(): void; // 重载2
// }
9.2.2 命名空间合并
命名空间可以相互合并,也可以与类、函数或枚举合并。
// 命名空间之间的合并
namespace Animals {
export class Zebra {}
}
namespace Animals {
export interface Legged {
numberOfLegs: number;
}
export class Dog {}
}
// 合并后的Animals命名空间包含Zebra、Dog和Legged
命名空间合并规则: - 导出的成员必须唯一 - 非导出成员只在原始命名空间中可见
namespace Animal {
let haveMuscles = true;
export function animalsHaveMuscles() {
return haveMuscles;
}
}
namespace Animal {
export function doAnimalsHaveMuscles() {
return haveMuscles; // Error: haveMuscles在此不可见
}
}
9.2.3 命名空间与类合并
命名空间可以与类合并,用于创建内部类或相关类型。
class Album {
label: Album.AlbumLabel;
}
namespace Album {
export class AlbumLabel {
constructor(public name: string) {}
}
}
// 使用
let album = new Album();
album.label = new Album.AlbumLabel("Interscope");
这种模式的用途: - 将辅助类/类型与主类组织在一起 - 实现静态属性的类型安全
// 静态属性的类型安全
class Handler {
static version: Handler.Version;
}
namespace Handler {
export enum Version {
V1 = "1.0",
V2 = "2.0"
}
}
Handler.version = Handler.Version.V1;
9.2.4 命名空间与函数合并
函数可以与命名空间合并,用于添加属性或创建可调用的命名空间。
function buildLabel(name: string): string {
return buildLabel.prefix + name + buildLabel.suffix;
}
namespace buildLabel {
export let suffix = "";
export let prefix = "Hello, ";
}
console.log(buildLabel("Sam")); // "Hello, Sam"
buildLabel.suffix = "!";
console.log(buildLabel("Sam")); // "Hello, Sam!"
这种模式常用于: - 创建带配置的函数 - 类似jQuery的可调用对象
// 类似jQuery的函数+命名空间模式
declare function $(selector: string): NodeListOf<Element>;
declare function $<T extends Element>(element: T): T;
declare namespace $ {
export function ajax(url: string, settings?: any): Promise<any>;
export const version: string;
export fn: {
extend(object: any): void;
};
}
// 使用
$(".item");
$.ajax("/api/data");
9.2.5 命名空间与枚举合并
命名空间可以与枚举合并,用于添加静态方法。
enum Color {
red = 1,
green = 2,
blue = 4
}
namespace Color {
export function mixColor(colorName: string) {
if (colorName == "yellow") {
return Color.red + Color.green;
}
if (colorName == "white") {
return Color.red + Color.green + Color.blue;
}
}
}
console.log(Color.mixColor("yellow")); // 3
9.2.6 类合并限制
类不能直接与其他类或变量合并。
class A {}
class A {} // Error: 重复的标识符
const B = class {};
class B {} // Error: 重复的标识符
但可以通过接口来扩展类的类型定义:
class A {
x: number = 1;
}
interface A {
y: number;
}
const a = new A();
a.y = 2; // OK
9.3 扩展第三方库
声明合并最常见的用途是扩展第三方库的类型定义。
9.3.1 扩展全局对象
// 扩展Window对象
declare global {
interface Window {
myLib: {
version: string;
doSomething(): void;
};
}
}
// 使用
window.myLib.doSomething();
9.3.2 扩展模块
// 扩展express的Request对象
import { Request } from "express";
declare module "express" {
interface Request {
user?: {
id: string;
name: string;
};
requestTime: number;
}
}
// 在express中间件中使用
app.use((req, res, next) => {
req.requestTime = Date.now();
next();
});
9.3.3 扩展现有类
// 扩展Array原型(类型层面)
declare global {
interface Array<T> {
last(): T | undefined;
first(): T | undefined;
}
}
// 运行时实现
if (!Array.prototype.last) {
Array.prototype.last = function() {
return this[this.length - 1];
};
}
// 使用
const arr = [1, 2, 3];
console.log(arr.last()); // 3
9.4 模块增强模式
9.4.1 声明文件中的合并
在.d.ts文件中,声明合并对于库作者特别有用。
// mylib.d.ts
// 主声明
export interface Config {
apiUrl: string;
timeout: number;
}
export function initialize(config: Config): void;
// 扩展声明
export namespace Config {
export interface AdvancedOptions {
retries: number;
cache: boolean;
}
}
9.4.2 拆分大型接口
// user.types.ts
export interface User {
id: string;
name: string;
}
// user-profile.types.ts
import { User } from "./user.types";
declare module "./user.types" {
interface User {
profile: {
avatar: string;
bio: string;
};
}
}
// 使用
const user: User = {
id: "1",
name: "John",
profile: {
avatar: "avatar.jpg",
bio: "Hello"
}
};
9.5 全局模块扩展
使用declare global可以在模块中向全局作用域添加声明。
// utils.ts
export {};
declare global {
interface String {
capitalize(): string;
}
function assert(condition: any, msg?: string): asserts condition;
}
// 实现
String.prototype.capitalize = function() {
return this.charAt(0).toUpperCase() + this.slice(1);
};
// 使用
import "./utils";
console.log("hello".capitalize()); // "Hello"
9.6 实际应用案例
9.6.1 插件系统类型定义
// 核心应用类型
export interface Application {
name: string;
plugins: Plugin[];
use(plugin: Plugin): void;
}
export interface Plugin {
name: string;
install(app: Application): void;
}
// 扩展点声明
export namespace Application {
export interface ComponentRegistry {
[name: string]: any;
}
}
// 插件可以扩展Application
// plugin.d.ts
declare module "./app" {
interface Application {
components: Application.ComponentRegistry;
registerComponent(name: string, component: any): void;
}
}
9.6.2 状态管理库扩展
// store.ts
export interface State {
count: number;
}
export interface Store {
state: State;
commit(mutation: string, payload?: any): void;
}
// mutations.ts
export const mutations = {
increment(state: State) {
state.count++;
}
};
// 扩展Store以支持类型安全的commit
declare module "./store" {
interface Store {
commit(type: "increment"): void;
commit(type: "add", payload: number): void;
}
}
9.6.3 API客户端类型扩展
// api-client.ts
export interface ApiClient {
get<T>(url: string): Promise<T>;
post<T>(url: string, data: any): Promise<T>;
}
export class ApiClientImpl implements ApiClient {
async get<T>(url: string): Promise<T> {
// 实现
return {} as T;
}
async post<T>(url: string, data: any): Promise<T> {
// 实现
return {} as T;
}
}
// 特定API模块扩展客户端
declare module "./api-client" {
interface ApiClient {
getUser(id: string): Promise<User>;
createUser(data: CreateUserData): Promise<User>;
}
}
// 实现扩展
ApiClientImpl.prototype.getUser = function(id: string) {
return this.get(`/users/${id}`);
};
9.7 注意事项
9.7.1 合并顺序
接口成员的顺序遵循声明的顺序,函数重载有特殊规则。
interface Example {
foo(x: number): void; // 1
}
interface Example {
foo(x: string): void; // 2
}
interface Example {
foo(x: any): void; // 3 (实现签名)
}
// 实际重载顺序: 2, 1, 3
9.7.2 类型兼容性
合并的成员类型必须兼容。
interface A {
x: number;
}
interface A {
x: string; // Error: 类型不兼容
}
9.7.3 可见性规则
命名空间合并时,非导出成员保持私有。
namespace Outer {
let privateVar = 1;
export function getPrivate() {
return privateVar;
}
}
namespace Outer {
export function tryAccess() {
// 可以访问privateVar,因为这是同一个命名空间
return privateVar;
}
}
9.8 小结
声明合并是TypeScript类型系统的一个强大特性,它允许:
1. 接口合并:创建完整的类型定义 2. 命名空间合并:组织和拆分代码 3. 命名空间与类/函数/枚举合并:创建静态成员和可调用对象 4. 模块扩展:安全地扩展第三方库类型 5. 全局扩展:向全局对象添加类型定义
合理使用声明合并可以提高代码的可维护性和可扩展性,特别是在大型项目和库开发中。但需要注意合并规则,避免类型冲突。
