type-safe-monkey-patching について
グローバルオブジェクトや DOM 要素にプロパティを追加する TypeScript 開発者向けのスキルです。any を使わず、インターフェース拡張やスコープを限定した型アサーションで型安全に扱う方法を紹介します。
主な機能
- 01インターフェース拡張でグローバルプロパティを宣言
- 02未設定の可能性を undefined で表現
- 03独自の型アサーションで変更範囲を限定
- 04DOM 要素の型を拡張
- 05モジュール内状態などの代替案を検討
活用シーン
- 01window や document に型付きプロパティを追加
- 02DOM 要素にアプリケーションデータを付与
- 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 がない | 競合状態を見えなくします |
よくある正当化
「プロパティは 1 つだけです」
実際のところ:as any はすべて、実行時エラーの原因になり得ます。今日の 1 つのプロパティが明日には 20 個になり、それらすべてが未チェックのままになります。
「注意して使います」
実際のところ:同僚はそのプロパティを知りません。入力を間違えるでしょう。型安全な拡張なら、契約が文書化され、強制されます。
「拡張は定型コードが多すぎます」
実際のところ:3 行のインターフェース宣言により、入力ミスや型の誤りのデバッグに何時間も費やさずに済みます。
「レガシーコードなので、後で直します」
実際のところ:インターフェースの拡張にかかる時間は 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 またはデータ属性 |
| ライブラリがグローバルを必要とする | インターフェースの拡張とドキュメント化 |
要点
JavaScript でモンキーパッチを使用する場合、TypeScript では明示的な型付けが必要です。グローバルプロパティにはインターフェースの拡張を、スコープ限定の変更にはカスタム型アサーションを使用してください。TypeScript の目的を損なうため、as any は決して使用しないでください。モンキーパッチが本当に必要かどうかを常に検討してください。多くの場合、よりすっきりとしたアーキテクチャ上の解決策があります。
参考資料
- Dan Vanderkam 著『Effective TypeScript』第 2 版
- 項目 47:モンキーパッチには型安全なアプローチを優先する