用户工具

站点工具


typescript:第九章声明合并

第九章 声明合并

声明合并(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. 全局扩展:向全局对象添加类型定义

合理使用声明合并可以提高代码的可维护性和可扩展性,特别是在大型项目和库开发中。但需要注意合并规则,避免类型冲突。

typescript/第九章声明合并.txt · 最后更改: 127.0.0.1