TL;DR

  • 組み込みオブジェクトにプロパティを追加するモンキーパッチは、グローバル変数と同じなので避ける。
  • 避けられないときは、any ではなくオーグメンテーションか狭い型アサーションを使う。

内容

JavaScript のモンキーパッチ

JavaScript のモンキーパッチは、組み込みオブジェクトにプロパティを追加することを指す。

RegExp.prototype.monkey = "Capuchin"
console.log(/123/.monkey) // 'Capuchin'

これはデータをグローバル変数に置くのと変わらない。予期しない依存関係が生まれ、関数を呼ぶたびに副作用を気にしなければならなくなる。

TypeScript では、存在しないプロパティにアクセスすると型エラーになる。any を使えば回避できるが、型安全性や言語サービスのサポートが失われる。

RegExp.prototype.monkey = "Capuchin" // Property 'monkey' does not exist on type 'RegExp'.

回避策

最も良いのは、データを組み込みオブジェクトから追い出し、モンキーパッチをなくすことである。

モンキーパッチを必要とするライブラリを使うときや移行中など、それができない場合は、オーグメンテーションを使う(項目71 モジュールオーグメンテーションを使って型を改善する)。

declare global {
  interface Window {
    user: User
  }
}
 
document.addEventListener("DOMContentLoaded", async () => {
  const resp = await fetch(`/api/users/current-user`)
  const user = (await resp.json()) as User
  window.user = user // OK
})
 
export function greetUser() {
  alert(`Hello ${window.user.name}`) // OK
}

この方法は、any を使うより次の点で優れている。

  • 型安全性が得られる
  • プロパティにドキュメントを付けられる(項目68 APIのコメントにTSDocを使う)
  • 自動補完などの言語サービスを使える
  • どんなモンキーパッチをしているかの正確な記録が残る

オーグメンテーションの問題点と回避策

オーグメンテーションには 2 つの問題がある。

1 つ目は、プロパティが追加された後だけにオーグメンテーションを適用する方法がないことである。そのため競合状態が起こりうる。たとえば、window.user がセットされる前に greetUser を呼び出せてしまう。対策として、型定義に undefined を含める(user?: User)。または、HTML のインラインスクリプトで先にプロパティを追加し、必ず存在するようにする。

<script type="text/javascript">
  window.user = { name: "Bill Withers" }
</script>
<script src="your-code.js"></script>

2 つ目は、型定義がグローバルに適用され、グローバルスコープを汚すことである。対策として、狭い型アサーションを使う。

type MyWindow = typeof window & { user?: User }
 
document.addEventListener("DOMContentLoaded", async () => {
  const resp = await fetch(`/api/users/current-user`)
  const user = (await resp.json()) as User
  ;(window as MyWindow).user = user
})
 
export function greetUser() {
  alert(`Hello ${(window as MyWindow).user.name}`)
}

ただし、追加したプロパティを参照するたびにアサーションが必要で面倒である。また、(window as any) と書けないように Linter で強制する必要がある。

キーワード