mcpField
T
type-safe-monkey-patching

type-safe-monkey-patching

github.com/marius-townhouse开发644 次安装38 周前更新
代码仓库

关于 type-safe-monkey-patching

面向需要在 TypeScript 中扩展全局对象或 DOM 元素的开发者,介绍保留类型安全的猴子补丁做法。内容包括接口增强、局部类型断言及避免使用 any 的替代方案。

主要功能

  1. 01通过接口增强声明全局属性
  2. 02用 undefined 表达属性可能缺失
  3. 03以自定义类型断言限制作用范围
  4. 04为 DOM 元素扩展类型
  5. 05介绍模块状态等替代方案

适用场景

  1. 01为 window 或 document 增加类型化属性
  2. 02给 DOM 元素附加应用数据
  3. 03迁移依赖全局状态的 JavaScript 代码
  4. 04避免猴子补丁使用 any

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/marius-townhouse/effective-typescript-skills --skill type-safe-monkey-patching

安装后在 Claude Code 中输入/type-safe-monkey-patching确认已加载。

详细介绍

使用类型安全的方式进行猴子补丁

概述

猴子补丁——在运行时向内置对象添加属性——是一种在 TypeScript 中容易引发问题的 JavaScript 模式。TypeScript 不知道你已添加到 window、document 或 DOM 元素的属性,因此会导致类型错误。虽然 as any 是快速解决方法,但它会完全牺牲类型安全性。还有更好的方法,既能在描述运行时修改的同时保留类型检查。

何时使用此技能

  • 向 window 或 document 添加全局变量
  • 向 DOM 元素附加数据
  • 使用需要全局状态的库(jQuery、D3)
  • 迁移使用猴子补丁的 JavaScript 代码
  • 在全局对象上存储应用状态

铁律

切勿使用 (obj as any).property 进行猴子补丁。应使用接口扩充或范围更窄的类型断言,以保留类型安全性。

检测

留意以下模式:

// RED FLAGS - Untyped monkey patching
(window as any).myApp = { /* ... */ };
(document as any).user = currentUser;
(element as any).customData = data;

// These lose all type safety
(window as any).usr = user;  // Typo not caught
(window as any).user = /regex/;  // Wrong type not caught

类型安全的方法

方法 1:接口扩充(全局)

当属性确实是全局的且始终可用时,最适合使用:

// types/global.d.ts
interface User {
  name: string;
  id: number;
}

declare global {
  interface Window {
    /** The currently logged-in user */
    user: User;
  }
}

// Usage - fully type-safe
window.user = { name: "Alice", id: 1 };  // OK
window.user = { name: "Alice" };  // Error: missing 'id'
window.usr = user;  // Error: typo caught
console.log(window.user.name);  // Autocomplete works
方法 2:使用 undefined 进行扩充(更安全)

当全局变量可能尚未设置时:

declare global {
  interface Window {
    /** The currently logged-in user - may not be set */
    user: User | undefined;
  }
}

// Forces handling of undefined
function greetUser() {
  if (window.user) {
    alert(`Hello ${window.user.name}!`);  // OK after check
  }
}

// Or use optional chaining
alert(`Hello ${window.user?.name ?? 'Guest'}!`);
方法 3:自定义类型断言(限定范围)

当你不想污染全局 Window 类型时:

type MyWindow = typeof window & {
  /** The currently logged-in user */
  user: User | undefined;
};

// Assignment
(window as MyWindow).user = currentUser;

// Access
const user = (window as MyWindow).user;
if (user) {
  console.log(user.name);
}
方法 4:DOM 元素数据(类型安全)

用于向 DOM 元素附加数据:

// Define extended element type
interface ExtendedElement extends HTMLElement {
  customData?: {
    initialized: boolean;
    value: number;
  };
}

// Use with type assertion
const el = document.getElementById('myElement') as ExtendedElement;
el.customData = { initialized: true, value: 42 };

// Better: Use data attributes or WeakMap
const elementData = new WeakMap<HTMLElement, { initialized: boolean; value: number }>();
elementData.set(el, { initialized: true, value: 42 });

抵抗压力的操作规范

当有人施压,要求你为了快速进行猴子补丁而使用 as any 时:

  1. 评估需求:猴子补丁真的有必要吗?是否可以通过重构解决?
  2. 选择方法:全局场景使用接口扩充,限定范围的场景使用自定义类型
  3. 添加 undefined:除非你确定该值始终存在
  4. 记录说明:添加 JSDoc 注释,解释猴子补丁
  5. 考虑替代方案:能否改用模块级变量?

危险信号

反模式问题所在
(window as any).prop没有类型安全性,无法捕获拼写错误
(document as any).data无法捕获错误类型
为页面专属数据进行全局扩充对可用性做出了不实声明
扩充时未包含 undefined掩盖竞态条件

常见借口

“只是一个属性而已”

事实:每个 as any 都可能引发运行时错误。今天一个属性,明天就可能变成二十个,而且都不受检查。

“我会小心的”

事实:同事不会知道这个属性的存在。他们可能会拼错。类型安全的扩充既能记录约定,也能确保约定得到遵守。

“扩充需要写太多样板代码”

事实:三行接口声明就能省下数小时的时间,避免排查拼写错误和类型错误。

“这是遗留代码,我们以后再修”

事实:接口扩充与 as any 花费的时间相同,却能立即带来安全性。

猴子补丁的更好替代方案

进行猴子补丁前,请考虑以下方案:

// 1. Module-level state
let currentUser: User | undefined;
export function setUser(user: User) { currentUser = user; }
export function getUser() { return currentUser; }

// 2. Context/dependency injection
class AppContext {
  user: User | undefined;
}
const context = new AppContext();

// 3. React Context, Vue provide/inject, etc.
const UserContext = createContext<User | undefined>(undefined);

// 4. WeakMap for DOM data
const elementState = new WeakMap<Element, ElementState>();

快速参考

场景推荐方法
全局变量始终可用接口扩充
全局变量有时可用使用 | undefined 进行扩充
页面专属全局变量自定义类型断言
DOM 元素数据WeakMap 或数据属性
库要求使用全局变量接口扩充 + 文档说明

核心要点

在 TypeScript 中进行 JavaScript 猴子补丁,需要显式指定类型。全局属性应使用接口扩充,限定范围的修改则应使用自定义类型断言。切勿使用 as any——它会让 TypeScript 失去意义。始终考虑猴子补丁是否真的有必要;很多时候,架构上有更简洁的解决方案。

参考资料

  • Dan Vanderkam 著《Effective TypeScript》第 2 版
  • 条目 47:优先采用类型安全的猴子补丁方式

评论

支持 Markdown