問題点:ネストしたプロパティへのアクセスは壊れやすい
ネストしたオブジェクトの中身を取り出す処理は、普段は問題なく動きます。ただし、途中の階層が存在しなかった瞬間にエラーになります。
user.address が undefined なので、そこから .city を読み取ろうとすると TypeError: Cannot read properties of undefined が投げられます。APIレスポンス、パース済みのJSON、DOMクエリなど、実際のデータには存在するかどうか分からないフィールドが山ほどあります。そして、各階層ごとに防御的なチェックを書いていると、コードはすぐにごちゃごちゃしてきます。
const city = user && user.address && user.address.city;
読めなくはないですが、ネストが2階層までの話。4階層にもなると地獄です。こういうときに ?. 演算子を使うとスッキリ書けます。
?. は null と undefined で短絡評価される
JavaScript のオプショナルチェーン演算子(?.)は、手前の値が null でも undefined でもないときだけプロパティを読み取ります。もしそうでなければ、式全体が undefined になってそこで評価が止まります。
1行目は普通に name を取り出せます。2行目では user.address が undefined なので、そこで中断し、チェーンの残りは評価されません。3行目は本来なら undefined に対して .toUpperCase() を呼ぶことになってエラーですが、?. のおかげで全体が静かに undefined になります。
イメージとしては、?. は「自分の手前が null か undefined だったら諦めて undefined を返す。そうじゃなければ処理を続ける」という意味だと覚えておけばOKです。
配列や関数呼び出しにも使えるオプショナルチェーン
書き方は3パターンありますが、考え方はどれも同じです。
- 配列アクセスや動的プロパティアクセスには
?.[...]を使います。 - 関数かどうか分からないものを呼び出すときは
?.()です。 - 通常のプロパティアクセスなら
?.nameですね。
この3つはいずれも同じようにショートサーキット(短絡評価)します。たとえば user?.notAMethod?.() は、notAMethod が存在しなくてもエラーになりません。2つ目の ?. が undefined を検知した時点で処理が止まるからです。
この書き方は、オプショナルなコールバックを扱うときに特に便利です。
if (typeof onDone === "function") のようなガード節を呼び出しのたびに書く必要はもうありません。
ショートサーキットが発動するのは null と undefined だけ
ここが意外と勘違いしやすいポイントです。?. が反応するのは nullish な値だけ。0、""、false、NaN といった他の falsy な値は、オブジェクトとして普通にチェーンできてしまいます(オートボクシングが効く範囲で、という話ですが):
data.count は 0 です。falsy ではありますが nullish ではないので、?.toFixed(2) がそのまま実行され "0.00" が返ります。これを && と比べてみましょう。
&& 版が 0 を返すのは、data.count が falsy なので短絡評価が効くからです。一方で ?. 版が "0.00" を返すのは、0 が nullish ではないからですね。0 でも処理を止めたいなら && が正解。「値が存在しないときだけ止めたい」のであれば、?. の出番です。
? を置く位置で挙動が変わる
オプショナルチェーン演算子 ?. が守ってくれるのは、その 直前 の値だけです。後ろではありません。なので、欠けている可能性がある階層に付けるのがポイントです。
ここでは 3 つともきちんと動きますが、これは値が欠けていないからです。もし config.server が undefined になる可能性があるなら、config.server?.host と書く必要があります。server の手前に ?. を置いても意味がありません。問題は、存在しない server から .host を読もうとすることだからです。
コツとしては、その 手前 にある値が本当に null や undefined になり得る箇所だけ に ?. を付けることです。「念のため」と全部のドットに ?. を付けて回ると、バグを隠してしまう上にコードもノイズだらけになります。
?. 経由での代入はできない
オプショナルチェーンは読み取り専用です。代入の左辺には使えません。
user?.address?.city = "Paris"; // SyntaxError
考えてみれば当然の挙動ですよね。undefined のプロパティに代入するって、そもそも意味が通らないわけですから。もし親オブジェクトが存在するときだけ値をセットしたい場合は、こんなふうに書き下してあげましょう。
どんなときに使う? 逆に使わない方がいいのは?
?. が真価を発揮するのは、値が「本当に存在しないこともある」ケースです。
- フィールドが含まれたり含まれなかったりする API レスポンス
- DOM の取得処理:
document.querySelector(".banner")?.remove() - 渡されるとは限らないコールバック:
options.onError?.(err) - 途中の結果が
nullになり得るライブラリのメソッドチェーン
逆に、値が「絶対に存在するはず」のところに ?. を使うのは間違いです。エラーを黙らせるために安易に ?. を撒くと、「42 行目で TypeError」という見つけやすいバグが、「3 つ先の関数で変数がなぜか undefined」という静かなバグに化けてしまいます。本来あるべきものが無いなら、堂々と例外を投げさせましょう。スタックトレースはむしろありがたい味方です。
実践的な例
欠損があり得る API レスポンスから値を取り出す例を見てみましょう。
?. は不確かさを1段ずつ処理してくれます。欠けている値があっても、avatarUrl はきれいに undefined になります。onClick?.() は、ハンドラーがあれば呼び出し、なければ何もしません。
displayName の行にある ?? にも注目してみてください。これはこのパターンのもう半分です。?. は値が欠けているときに undefined を返してくれますが、?? は 0 や "" のような正当な falsy 値を誤って弾かずに、デフォルト値を差し込むためのものです。
次は: Nullish 合体演算子
?. と ?? は、同じ発想を別々の問題に適用したものです。どちらも null と undefined だけを「欠けている」とみなし、それ以外の falsy 値はそのまま尊重します。次の章では、?? でまともなデフォルト値を扱う方法と、|| が長年こっそり間違った挙動をしてきた理由を見ていきます。
よくある質問
JavaScriptの ?. は何をする演算子ですか?
?. は、直前の値が null や undefined でないときだけ、プロパティ・配列の要素・メソッドにアクセスする演算子です。もし null か undefined だった場合はそこで式全体がショートサーキットし、エラーを投げずに undefined を返します。例えば user?.address?.city と書いておけば、user や address が存在しなくても落ちません。
オプショナルチェーンはどんな場面で使うべき?
「値が存在しないこともあり得る」と明確にわかっているときに使うのがベストです。たとえばオプショナルなフィールドを含むAPIレスポンス、見つかるとは限らないDOM要素、渡されるかどうかわからないコールバック関数などですね。逆に、本来必ず存在しているはずの値に ?. を付けてバグをごまかすのはNGです。そういうケースでは、値が無いこと自体が重要なサインなので、素直にエラーとして見えるようにしておくべきです。
?. と && はどう違いますか?
多くの場合は同じ結果になりますが、挙動の条件が違います。?. はショートサーキットするのが null と undefined のときだけ。一方 && は 0 や ''、false など、あらゆるfalsy値でショートサーキットします。たとえば obj && obj.count は count が 0 のときにも 0 を返しますが、これは意図せずfalsy判定が走ってしまうことがある書き方です。obj?.count なら null / undefined のときだけ止まるので、0 や空文字は素直にそのまま返ってきます。
配列や関数呼び出しにもオプショナルチェーンは使えますか?
使えます。arr?.[0] と書けば配列のインデックスに安全にアクセスできますし、fn?.() なら関数が存在するときだけ呼び出せます。ルールは同じで、?. の左側が null または undefined なら式全体は undefined になり、その後ろの処理は一切実行されません。