オプショナルチェーン ?.
オプショナルチェーン ?. とは
?.でネストした値を安全に取り出し、null/undefinedのときも例外を出さずにundefinedを返そう。
オプショナルチェーン ?. で深い値を安全に取り出す
JavaScript でとてもよく遭遇するエラーが、TypeError: Cannot read properties of undefined (reading 'name') です。これは user.profile.name のようにネストしたオブジェクトの値を取り出そうとして、途中の user.profile が undefined だったときに発生します。古い書き方では、各階層ごとに if で存在チェックをする必要があって、コードがどんどん長くなっていました。
この面倒を解決する演算子が オプショナルチェーン ?. です。ES2020 で追加され、書式は obj?.prop で「obj が null / undefined ならその場で undefined を返し、それ以外は通常通り prop を取り出す」という動きをします。
?.は「途中で止まる安全弁」です。連鎖の途中がnullやundefinedでも、エラーにせずundefinedを返してくれます。
文法の基本
書き方は、ピリオドの代わりに ?. を使うだけです。
JavaScript
const user = { name: "Naoki" };
console.log(user?.name); // "Naoki"
console.log(user?.profile?.bio); // undefined 途中で止まる
console.log(user?.profile?.bio?.toUpperCase()); // undefined 関数呼び出しも安全user.profile が undefined の時点で、続く ?.bio は 評価されず 全体が undefined になります。これを ?. 無しで書くと次のような恐ろしさです。
JavaScript
const user = { name: "Naoki" };
console.log(user.profile.bio); // TypeError で停止アプリケーションが落ちる原因になるので、ネストしたアクセスは原則として ?. を使うのが安全です。
3 つの使い方
?. は次の 3 つのパターンで使えます。
- プロパティアクセス —
obj?.prop - 配列の要素アクセス —
arr?.[0] - 関数呼び出し —
fn?.()
それぞれの例を見てみます。
JavaScript
const data = null;
console.log(data?.users); // undefined プロパティアクセス
console.log(data?.["users"]); // undefined ブラケット記法も OK
const arr = null;
console.log(arr?.[0]); // undefined 配列アクセス
const maybeFn = undefined;
console.log(maybeFn?.()); // undefined 関数呼び出しどれも「左が null / undefined なら止まる」という共通ルールです。これだけで、if (data && data.users && data.users[0]) のような長い守りのコードがほぼ消えます。
Mermaid で ?. の挙動を追う
user?.profile?.name の評価フローを図にすると次のとおりです。
各階層で「null か undefined か」を確認し、見つかった瞬間に undefined を返します。3 階層、4 階層と深くなっても同じパターンが繰り返されるだけなので、コードがネストしません。
?? と組み合わせると最強
?. と ?? を組み合わせると、「ネスト値を安全に取り出して、無ければデフォルト」という超頻出パターンが 1 行で書けます。
JavaScript
const user = null;
const displayName = user?.profile?.name ?? "ゲスト";
console.log(displayName); // "ゲスト"user?.profile?.name が undefined になっても、?? がデフォルト値 "ゲスト" を返すので、UI に表示する文字列が必ず確保できます。これは React や Vue のテンプレートでも超頻出のパターンです。
?.と??はセットで覚えると効果倍増です。undefinedの連鎖を??でキャッチするイメージを持ってください。
よくある間違い
?. の落とし穴を 3 つ紹介します。
?.を全部にバラまく —a?.b?.c?.d?.eのように毎階層に?.を付けると、本来null/undefinedであってはいけない場所のバグを 隠してしまう ことがあります。あくまでも「本当に欠ける可能性がある場所」だけに付ける、というのが理想です?.の右側に=を書く —obj?.prop = valueは構文エラーです。?.は 読み取り専用。代入はif (obj) obj.prop = value;のように分岐させる必要があります?.と?(三項演算子) を混同する —obj?.propは安全アクセス、obj ? a : bは条件分岐です。スペースの有無で意味が変わるので、エディタの色分けで区別する習慣を
もうひとつ、falsy と nullish の違いに引きずられないように注意してください。
JavaScript
const obj = { count: 0 };
console.log(obj?.count); // 0 正しく取れる
console.log(obj?.count || "なし"); // "なし" 0 が falsy で上書き
console.log(obj?.count ?? "なし"); // 0 のまま 正しい0 を正常な値として残したいなら、|| ではなく ?? を組み合わせるのが正解です。
?.は「左が nullish か」を見る演算子、??も「左が nullish か」を見る演算子。両者は 同じ判定基準 を共有しているのが嬉しいポイントです。
やってみよう
それでは課題に取り組みましょう。getCity(user) を完成させて、user.address.city を安全に取り出してください。途中の user や user.address が null / undefined でも例外を出さず、undefined を返してください。
書き方の方針は次のとおりです。
return user?.address?.city;の 1 行で完成if (user && user.address && user.address.city)のような古いガード書きを避ける- 引数が
nullでもundefinedでも、テストはundefinedを期待
?. を使いこなせるようになると、API レスポンスの取り扱いがぐっと楽になります。プロのフロントエンド開発では毎日のように使う必須演算子です。
よくある質問
Q. ブラウザと Node.js で動作は変わりますか?
A. JavaScript の構文・組み込み機能(配列・文字列・Promise 等)はどちらでも同じです。違いは DOM API(ブラウザ専用)と fs などのファイル系(Node 専用)など、実行環境固有の機能です。コアロジックは両方で動くため、テストもしやすくなります。
Q. ES6 以降の新機能は使っても大丈夫ですか?
A. 現代のブラウザ・Node.js は ES2020 以降の機能をほぼサポートしています。古い IE 等を対象にする必要が無ければ、let/const、アロー関数、分割代入、async/await を積極的に使ってください。トランスパイル(Babel/Vite)が必要なケースは年々減っています。
Q. TypeScript に進む価値はありますか?
A. 型情報があるとリファクタやエディタ補完が劇的に効くため、規模が大きくなるなら TypeScript への移行を強くおすすめします。最初は any を許容しつつ徐々に型を付ければ、JS の知識をそのまま活かせます。
次のレッスン
次は ビット演算で偶数判定 で、`? を学びます。
事前確認 — 進む前に次の 3 つができることを確認しましょう。
- ?. 演算子 の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. ?. 演算子 とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
- 関数名は
getCity、引数はuserの 1 つにすること - プロパティアクセスには
?.を使うこと if文や&&ガードを使わずに 1 行で書くこと
入出力例
test-cases.txt
getCity({"address":{"city":"Tokyo"}}) → "Tokyo"
getCity({}) → undefined
getCity({"address":{}}) → undefined
getCity(null) → undefined
getCity({"address":{"city":"Osaka"}}) → "Osaka"