やあ。最近、プロダクトの規模が膨らみすぎて「どこで誰がこのステートを書き換えたんだ…?」という不毛なバグハンティングに時間を溶かしてないかい?
中級から一段上のシニアへと駆け上がるエンジニアが必ず直面するのが、この「意図しないプロパティの書き換え(ミュータビリティの暴走)」という悪夢だ。特にTypeScriptを導入していれば `readonly` や `as const` で守られるけれど、レガシーなJSコードベースや、あえてJSDocをメインに据えた軽量なモジュール開発では、油断するとすぐにオブジェクトが汚染されてしまう。
そこで今回は、TypeScriptを使わずとも、JSDocの `@readonly` アノテーションを駆使してIDE(VSCodeなど)上でバグの芽をバキバキに摘み取る実務テクニックを伝授しよう。ブラウザの裏側の挙動も含めて、本質を叩き込んでいくから、ついてきてくれよ。
—
1. JavaScriptにおける「読み取り専用」の現実と、JSDocがもたらす恩恵
まず前提として、JavaScriptのオブジェクトはデフォルトで「何でもあり」のオープンな世界だ。`const` で宣言した変数であっても、オブジェクトのプロパティそのものは平気で書き換えられる。
const user = { name: ‘Alice’ };
user.name = ‘Bob’; // 普通に通る。constが守るのは「再代入」だけで、「プロパティの変更」ではない。
これを防ぐために、標準APIには `Object.freeze()` が存在する。
裏側の処理:`Object.freeze()` とブラウザの最適化
`Object.freeze(obj)` を呼ぶと、JavaScriptエンジン(V8など)は内部のオブジェクトのフラグを書き換えて、プロパティの追加・削除・変更を禁止する(`extensible: false`, `writable: false`, `configurable: false` になる)。
runtime(実行時)の安全性としては最高だが、実務でこれをやると「厳格モード(strict mode)下では、書き換えようとした瞬間に例外(TypeError)がスローされる」という副作用がある。開発中のちょっとしたミスでアプリ全体がクラッシュするのは、ユーザー体験としても避けたいところだ。
そこで登場するのが、JSDocの `@readonly` だ。
これは実行時ではなく、開発時(IDEの静的解析のレイヤー)で私たちを救ってくれる。ブラウザのメモリやパフォーマンスを一切汚さずに、コードを書いているまさにその瞬間にエディタが赤く警告を出してくれるという、現代フロントエンド開発の必須教養なのだ。
—
2. 実践! `@readonly` を使った堅牢なオブジェクト定義
百聞は一見にしかず。実務のコードベースでそのまま使える、綺麗なサンプルを見ていこう。
エディタ(VSCode等)で開けば、誤った代入に対して即座に波線(エラー警告)が出るはずだ。
/
- @typedef {Object} SystemConfig
- @property {string} API_BASE_URL – APIのエンドポイント
- @property {number} TIMEOUT_MS – 通信タイムアウト時間
- @property {boolean} DEBUG_MODE – デバッグモードのフラグ
/
/
- アプリケーションのグローバル設定
- @type {SystemConfig}
- @readonly
/
export const CONFIG = {
API_BASE_URL: ‘https://api.example.com/v1’,
TIMEOUT_MS: 5000,
DEBUG_MODE: process.env.NODE_ENV !== ‘production’,
};
// ———————————————————
// ここから下で「うっかりミス」をやってしまった場合の実例
// ———————————————————
// ❌ 警告(またはエラー): Cannot assign to read-only property ‘API_BASE_URL’ of object ‘#
さらに実戦的なTips:オブジェクト内の「特定プロパティ」だけを保護する
先ほどの例はオブジェクト全体を `readonly` にしたが、実務では「設定オブジェクト全体の構造は変えたくないが、一部の状態は変えたい」、あるいは「特定のプロパティだけ死守したい」というケースの方が多い。
そんな時は、個別のプロパティに対して `@readonly` を付与するのがプロの技だ。
/
- @typedef {Object} UserSession
- @property {string} readonly id – ユーザーID(絶対に書き換わっては困る)
- @property {string} readonly email – メールアドレス(変更は専用のフローを通るべき)
- @property {string} role – 権限(セッション中で昇格・降格することがあるためwritable)
/
/
- 現在のログインセッション情報
- @type {UserSession}
/
let currentSession = {
id: ‘usr_99887766’,
email: ‘architect@example.com’,
role: ‘editor’,
};
// ✅ これはOK(roleはreadonly指定されていないため)
currentSession.role = ‘admin’;
// ❌ エディタが警告! IDは絶対に書き換えさせない
// currentSession.id = ‘usr_hacked_001’;
どうだい? TypeScriptの `readonly id: string` とほぼ同等の恩恵を、JSDocの構文だけで純粋なJavaScript環境に持ち込めているのが分かるはずだ。
—
3. シニアが教える、現場で導入する際の注意点
この `@readonly`、非常に強力なんだが、実務の現場に導入する時にはいくつか気をつけておいてほしいポイントがある。
1. これは「静的解析の縛り」であって、実行時の壁ではない
先ほども言った通り、JSDocの `@readonly` はTypeScriptと同様にコンパイル・解析時(あるいはIDE上)のチェックだ。もしビルドプロセスをすり抜けて本番のJSが実行された場合、古いブラウザやプレーンなJS環境では普通に値が書き換わってしまう。
「絶対に破壊されたくないコアな定数オブジェクト」に対しては、やはり `Object.freeze()` との合わせ技(あるいはモジュールのスコープでカプセル化してgetterのみ公開する手法)を検討してほしい。
2. VSCodeの `Check JS` を有効にすべし
プロジェクトのルートにある `jsconfig.json`(または `tsconfig.json`)で、ちゃんとJSの型チェックが有効になっているか確認しよう。ここが抜けていると、せっかく書いた `@readonly` がただのコメントになってしまい、IDEが警告を出してくれなくなる。
{
“compilerOptions”: {
“checkJs”: true,
“allowJs”: true
}
}
—
まとめ
フロントエンドのコードベースが複雑化すればするほど、「変えていいもの」と「変えてはいけないもの」の境界線をコード上で明確にすることが、エンジニアリングの質を決めるといっても過言ではない。
TypeScriptへの全面移行が難しいプロジェクトであっても、今日紹介したJSDocの `@readonly` なら、明日の朝からでもプロダクトに導入できる。
チームメンバーの「うっかり」をシステムで防ぎ、レビュー時の無駄な指摘を減らして、もっとクリエイティブな実装に時間を割こうぜ。
さて、次のアーキテクチャの課題に移ろうか。何か質問があればいつでも声をかけてくれ。

コメント