【テクニカル・上級編】 @readonlyによるイミュータブルなプロパティ定義 – JavaScript実践ガイド

@readonlyが救う巨大アプリケーションの未来:型のない世界でイミュータブルを守り抜く実務アーキテクチャ

こんにちは。フロントエンドの現場で数々のスパゲッティコードと戦い続けてきたチーフアーキテクトの私だ。

モダンなJavaScript開発において、TypeScriptへの完全移行はもはや常識……と言いたいところだが、現実はそう甘くない。既存の巨大なJSコードベース、サードパーティ製ライブラリの型定義のほころび、あるいは動的なメタプログラミングを多用するコアロジックにおいて、私たちは未だにプレーンなJavaScript(JSDoc)と向き合わなければならない瞬間がある。

特に「オブジェクトの予期せぬ書き換え」は、大規模アプリケーションにおいて幾度となく私たちを夜勤送りにしてきた悪魔の所業だ。フレームワークのリアクティブシステムがバグり、非同期処理の競合によって状態が汚染され、どこで誰がそのプロパティをいじったのか分からない。ReduxやZustandといった状態管理の導入だけでは、オブジェクトの内部深くまでイミュータブルを強制することはできない。

そこで今回は、JSDocの `@readonly` タグにスポットを当て、これが単なる「IDEへの甘い警告」にとどまらず、V8などのJSエンジンとの付き合い方、そしてメモリ効率やレンダリング最適化にどう寄与するのか、私の現場の知見を総動員して深掘りしていこう。

—

1. なぜプレーンなJSで `@readonly` なのか?

TypeScript全盛の時代に、なぜ今さらJSDocなのかと首をかしげる者もいるだろう。だが、考えてみてほしい。コンパイルステップを踏まない軽量なスクリプト群、あるいはJITコンパイラの挙動を極限までコントロールしたいコアなライブラリ開発において、プレーンなJavaScriptの機動力は依然として最強だ。

JSDocの `@readonly` は、単にVS CodeなどのIDEに「ここ書き換えたら赤く波線を引いてね」と伝えるだけの装飾ではない。これは、開発チーム全体に対する「ここにはドメインの核心がある。安易な破壊的変更(Mutation)を許すな」という強烈な契約(Contract)なのだ。

実行時の安全性をどう担保するか

まず大前提として知っておくべきなのは、JSDocの `@readonly` は TypeScriptの `readonly` や C# の `readonly` と同様に、純粋なコンパイル時(静的解析時)のチェック でしかないということだ。本番環境で走る素のJavaScriptエンジン(V8など)は、`@readonly` の注釈を華麗に無視して値を書き換えてしまう。

/

  • @readonly
  • @type {number}

/
const MAX_RETRY_COUNT = 3;

// IDEやJSDocチェッカーはここで警告を出すが…
// MAX_RETRY_COUNT = 5;

// 実行時は普通に通る場合がある(strict modeやオブジェクトの定義方法による)

「なんだ、実行時に守ってくれないなら意味がないじゃないか」と思ったなら、それは浅い。真のシニアエンジニアは、この静的な型注釈と、JavaScriptのプリミティブ/オブジェクトの特性を組み合わせ、静的解析の美しさと実行時の堅牢性を両立させる。

—

2. メモリ効率とV8の隠れ最適化:イミュータブルがもたらす恩恵

ここで少し、ブラウザの内部挙動の話をしよう。V8をはじめとするモダンなJSエンジンは、オブジェクトの「形状(Hidden Class / Shape)」を最適化することでプロパティアクセスの高速化を図っている。

もし、あるオブジェクトのプロパティが自由奔放にあちこちで書き換えられ、動的に追加・削除されるとしたらどうなるか? エンジンは最適化をあきらめ、メモリ上のハッシュマップルックアップにフォールバックする。これはパフォーマンスの致命的な低下を招く。

ここで `@readonly` を意識した設計、すなわち「一度生成したら二度と形状も値も変えない」設計を徹底すると、エンジンはオブジェクトを「イミュータブルな定数オブジェクト」として強力にインラインキャッシュ(Inline Caching)の対象にできる。

さらに、非同期処理の文脈において、イミュータブルなデータ構造は「競合状態(Race Condition)」の特効薬となる。

/

  • ユーザーのセッション情報を表すイミュータブルな構造体

/
class UserSession {
/

  • @readonly
  • @type {string}

/
userId;

/

  • @readonly
  • @type {ReadonlyArray}

/
permissions;

/

  • @param {string} userId
  • @param {string[]} permissions

/
constructor(userId, permissions) {
this.userId = userId;
// 配列自体も凍結して防御を固める
this.permissions = Object.freeze([…permissions]);

// オブジェクト全体の形状を固定化し、V8のHidden Class最適化を誘発する
Object.seal(this);
}
}

このコードでは、JSDocの `@readonly` で開発時のIDE警告を出しつつ、コンストラクタ内で `Object.freeze` と `Object.seal` を組み合わせることで、実行時においても絶対に値が書き換わらない要塞を築いている。

—

3. 実務で光る! `@readonly` を活用したアーキテクチャパターン

では、実際のフロントエンド・アーキテクチャの中で、どのように `@readonly` を活かすべきか。具体的なユースペースを見ていこう。

ユースケース:グローバル設定とフィーチャートグル

大規模SPAにおいて、APIのエンドポイントや機能の有効/無効を切り替えるフィーチャートグルは、絶対にバグってはいけない領域だ。ここに `@readonly` を適用する。

/

  • アプリケーションのグローバル設定
  • @type {Object}

/
export const AppConfig = Object.freeze({
/

  • @readonly
  • @type {string}

/
API_BASE_URL: ‘https://api.enterprise-system.internal/v1’,

/

  • @readonly
  • @type {number}

/
TIMEOUT_MS: 5000,

/

  • @readonly
  • @type {boolean}

/
ENABLE_EXPERIMENTAL_RENDERER: false
});

// 万が一、別のモジュールで以下のようなコードを書いた瞬間、
// TypeScriptの言語サーバー(またはJSDocプラグイン)が即座にエラーを吐く
// AppConfig.TIMEOUT_MS = 10000; -> [Error] Cannot assign to ‘TIMEOUT_MS’ because it is a read-only property.

このアプローチの美しいところは、TypeScriptへ完全移行するコストを払わなくとも、tsconfig.jsの `checkJs: true` を有効にするだけで、プロジェクト全体で強固な型チェックとイミュータビリティの強制が手に入る点にある。

—

4. レンダリング負荷の軽減とフレームワーク連携の極意

ReactやVueなどのモダンなUIライブラリにおいて、不要な再レンダリング(Re-rendering)はパフォーマンス上の最大の敵だ。

状態管理や親コンポーネントから渡されるプロパティ(Props)が意図せずミューテートされた場合、フレームワークの差分検出(Diffing)アルゴリズムが混乱し、本来走るべきではないコンポーネントツリー全体のリペイントを引き起こす。

ここで、すべてのコンテキストやストアのステート定義に `@readonly` を徹底するとどうなるか。

1. 開発者の認知負荷の軽減: 「このオブジェクトのプロパティは変更してはいけない」ということがコード補完の瞬間にわかるため、不必要なクローンや場当たり的な値書き換えコードが撲滅される。
2. 参照透過性の向上: 値が変わらないことが保証されるため、メモ化(`React.memo` や `useMemo`)のヒット率が劇的に跳ね上がる。無駄な参照比較のコストが消え去るのだ。

/

  • @typedef {Object} ProductItem
  • @property {readonly string} id
  • @property {readonly string} name
  • @property {readonly number} price

/

/

  • 商品カードコンポーネント(メモ化前提)
  • @param {Object} props
  • @param {ProductItem} props.product

/
export const ProductCard = React.memo(({ product }) => {
// product の中身がイミュータブルであることが保証されているため、
// 浅い比較 (shallow equal) のみでメモ化の判定が確実に行える。
return (

{product.name}

¥{product.price.toLocaleString()}

);
});
ProductCard.displayName = ‘ProductCard’;

JSDocのプロパティ定義における `readonly string` のような記述(あるいは `@readonly` タグの併用)は、コンポーネントに渡るデータの「純度」を極限まで高めてくれる。

—

5. チーフアーキテクトからの提言:泥臭さと美しさの調和

私たちが書くコードは、単に動けばいいというものではない。数ヶ月後、あるいは数年後に、全く異なるチームメンバーがそのコードを引き継ぎ、夜中の3時に障害対応でコードを覗き込むことになる。その時、そこに明確な「意図」と「制約」があるかどうかで、システムの寿命が決まる。

TypeScriptは強力だ。だが、すべてのプロジェクトがいきなりTypeScriptの厳格な型システムを受け入れられるわけではない。だからこそ、プレーンなJavaScriptの柔軟性を残しつつ、JSDocと `@readonly` を駆使して「意図しない変更」をコンパイル(およびIDE)レベルで封じ込める手法は、プロフェッショナルなフロントエンドエンジニアにとって知っておくべき強力な武器なのだ。

今日のまとめとして覚えておいてほしい。
「イミュータビリティは、コードの美しさのためではなく、システム全体の生存確率を上げるための防壁である」と。

さあ、今すぐエディタを開き、あなたのプロジェクトのグローバル変数や設定ファイルに `@readonly` を仕込んでみせろ。IDEの赤い波線が、あなたのアプリケーションを未来のバグから守り抜く盾となるはずだ。

コメント

タイトルとURLをコピーしました