关于 type-safe-monkey-patching
面向需要在 TypeScript 中扩展全局对象或 DOM 元素的开发者,介绍保留类型安全的猴子补丁做法。内容包括接口增强、局部类型断言及避免使用 any 的替代方案。
主要功能
- 01通过接口增强声明全局属性
- 02用 undefined 表达属性可能缺失
- 03以自定义类型断言限制作用范围
- 04为 DOM 元素扩展类型
- 05介绍模块状态等替代方案
适用场景
- 01为 window 或 document 增加类型化属性
- 02给 DOM 元素附加应用数据
- 03迁移依赖全局状态的 JavaScript 代码
- 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 时:
- 评估需求:猴子补丁真的有必要吗?是否可以通过重构解决?
- 选择方法:全局场景使用接口扩充,限定范围的场景使用自定义类型
- 添加 undefined:除非你确定该值始终存在
- 记录说明:添加 JSDoc 注释,解释猴子补丁
- 考虑替代方案:能否改用模块级变量?
危险信号
| 反模式 | 问题所在 |
|---|---|
(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:优先采用类型安全的猴子补丁方式