YASD-TECH
YASD TECH
# TypeScript

optional chainingの短絡はチェーン全体に効く(不要なnullチェックを足さない)

投稿日:2026/8/10

更新日:2026/8/10

ttitleImage

optional chaining の短絡はチェーン全体に効く

コードレビューで「payload?.items.forEach(...)forEach 側にも ?. を付けないと TypeError になる」と指摘された。結論から言うとこれは誤りで、修正は見送った。?.何を・どこまで 守るのかと、型で保証されている箇所に ?. を足すべきでない理由をまとめる。


結論

  • a?.b.c()?.チェーン全体を短絡するanull / undefined なら .b.c() も評価されないので TypeError にならない
  • ただし ?. が守るのは その ?. の左側だけb 自身が nullish になり得るなら、それは別の ?. の仕事
  • したがって「?. を足すべきか」は b の型が nullable かどうか だけで決まる。非 nullable なら足しても意味がなく、将来 nullable 化したときの型エラーまで潰してしまう

1. 題材:一括作成 mutation の結果を計測する

記事にタグをテキストから一括登録するダイアログ。作成に成功したタグごとに計測イベントを1件ずつ送る。

ts
const handleSubmit = async (text: string) => {
  const { data } = await createTags({ variables: { input: { articleId, text } } })
  const payload = data?.createTags

  if (payload?.failedTags.length) {
    showError(payload.failedTags)
  }

  // 1タグ = 1イベントとして、作成に成功したタグごとに発火する
  payload?.successfulTags.forEach((tag) => {
    trackEvent('tag_create', { tag_id: tag.id, article_id: articleId })
  })
}

これに対する指摘はこうだった。

mutation の戻り値が null になる(GraphQL error 等)と payloadundefined になり得ます。
payload?.successfulTagsundefined の場合に .forEach 呼び出しで TypeError になるため、
forEach 側にも optional chaining を付けるか、早期 return してください。

前半の「payload が null になり得る」は正しい。後半の「だから .forEach で TypeError になる」が誤りである。


2. ?. はチェーン全体を短絡する

optional chaining の仕様上の挙動は「左辺が nullish なら そこから右のチェーンを一切評価せず、式全体が undefined になる」。これを短絡(short-circuiting)と呼ぶ。

ts
const payload = undefined

payload?.successfulTags.forEach(f)
// → payload が nullish の時点で打ち切り
// → .successfulTags も .forEach(f) も評価されない
// → 式の値は undefined。例外は起きない
flowchart TD
    Start["payload?.successfulTags.forEach(f) を評価"] --> Check{"payload は<br/>null / undefined か"}
    Check -->|"はい"| Short["チェーン全体を短絡<br/>式の値は undefined"]
    Check -->|"いいえ"| Prop["payload.successfulTags を読む"]
    Prop --> Call["forEach(f) を呼ぶ"]
    Short --> Safe["TypeError は起きない"]
    Call --> Run["各要素で f を実行"]

payload?.successfulTags がまず undefined に評価されて、その undefined に対して .forEach を呼ぶ」というモデルが誤解の正体。実際には .forEach に到達しない

短絡は「?. を書いた位置から先」ではなく チェーンの終端まで 効く。だから a?.b.c.d.e()?. も1つで足りる。


3. ?. が守るのは左側だけ

一方で、?. は「その左側の値が nullish であること」しか守らない。

ts
const payload = { successfulTags: null }

payload?.successfulTags.forEach(f)
// → payload は non-null なので短絡しない
// → successfulTags(null)に対して .forEach → 💥 TypeError

つまり ?. の要不要は、置きたい位置の 左側の値が nullable かどうか で決まる。

書き方 payload が nullish successfulTags が nullish forEach が undefined
payload.successfulTags.forEach(f) 💥 💥 💥
payload?.successfulTags.forEach(f) ✅ 短絡 💥 💥
payload?.successfulTags?.forEach(f) 💥
payload?.successfulTags?.forEach?.(f)

?.() は「関数そのものが無い」ケース用

プロパティアクセスの ?. と、呼び出しの ?.() は守る対象が違う。

ts
props.onClose?.()    // onClose が undefined でも呼ばない(関数の有無を守る)
props.modal?.close() // modal が undefined でも close を呼ばない(オブジェクトの有無を守る)

4. では successfulTags は nullable なのか → 型を見る

判断材料は推測ではなく生成された型。GraphQL codegen が吐いた型はこうなっていた。

ts
export type CreateTagsMutation = {
  createTags?: {
    successfulTags: Array<{ id: string; name: string }>
    failedTags: Array<{ input: string; message: string }>
  } | null
}

successfulTags?| null も付いていない 非 nullable な配列。nullable なのは createTags(= payload)だけ。

元の SDL はこうなっている。

graphql
type Mutation {
  createTags(input: CreateTagsInput!): CreateTagsPayload
}

type CreateTagsPayload {
  successfulTags: [Tag!]!
  failedTags: [TagError!]!
}

! の位置がそのまま TS の nullability に対応する。

SDL 生成される TS nullable か
createTags: CreateTagsPayload createTags?: {...} | null ✅ payload 自体
successfulTags: [Tag!]! successfulTags: Array<{...}>
successfulTags: [Tag!] successfulTags?: Array<{...}> | null ✅ 配列自体
successfulTags: [Tag]! successfulTags: Array<{...} | null> ✅ 要素だけ

payload が存在すれば successfulTags は必ず配列。空配列にはなるが null にはならない。よって ?.forEach は到達不能な分岐を守る記述になる。


5. なぜ「payload はあるが successfulTags が null」が起きないのか

「サーバが壊れて null を返すかもしれない」と考えたくなるが、GraphQL の null 伝播(error propagation) がそれを防ぐ。

Non-Null 型(!)のフィールドの解決が失敗した場合、そのフィールドは null を返せない。仕様上、null は 最も近い nullable な祖先まで遡って伝播 する。

flowchart TD
    A["successfulTags の解決が失敗"] --> B{"successfulTags は<br/>Non-Null か"}
    B -->|"はい"| C["自身は null を返せない<br/>親へ伝播"]
    C --> D{"createTags は<br/>Nullable か"}
    D -->|"はい"| E["createTags 全体が null になる<br/>= payload が null"]
    E --> F["payload が null なので<br/>?. が短絡する"]

つまり successfulTags の取得に失敗したなら、観測できるのは「payload ごと null」であって「payload はあるが successfulTags が null」ではない。型が実態を正しく表している

レビュー指摘にあった「GraphQL error で payload が null になる」は、まさにこの伝播のこと。原因の認識は合っていて、?. が既にそれを守っている、というのが噛み合っていなかった点。


6. 「念のため ?.」を足さない方がよい理由

害が無いなら足しておけばいい、とはならない。

観点 不要な ?. を足すと
可読性 どの ?. が実際に効いているのか読み手が判別できなくなる
型の変更検知 後日スキーマが [Tag!] に変わっても 型エラーが出ず黙ってスキップ される
意図の表明 「ここは null になり得る」という誤った情報をコードに書き込むことになる
静的検査 型情報を使う lint では警告対象になる

3行目が実害として一番大きい。?. は「値が無い」と「処理をスキップした」を区別せず、何も起きないまま正常終了する。計測イベントのような、落ちないが送られないと気づけない処理では特に危ない。

lint で機械的に検出する

@typescript-eslint/no-unnecessary-condition(型情報を使う型付き lint が必要)が、非 nullish な値に対する ?. を報告してくれる。

Unnecessary optional chain on a non-nullish value.

「足すべきか」を人力で議論せずに済むので、有効化しておくと同種の指摘が発生しなくなる。


7. 指摘のうち採用すべきだった部分

?. の追加は不要だが、早期 return の提案は検討に値するpayload?. のままだと、mutation 自体が失敗したケースが黙って素通りするため。

ts
const payload = data?.createTags

if (!payload) {
  // mutation そのものが失敗している(GraphQL error など)
  showError('タグの作成に失敗しました')
  return
}

if (payload.failedTags.length) {
  showError(payload.failedTags)
}

// ここまで来れば payload は non-null なので ?. すら要らない
payload.successfulTags.forEach((tag) => {
  trackEvent('tag_create', { tag_id: tag.id, article_id: articleId })
})
書き方 payload が null のとき 向いている場面
payload?.successfulTags.forEach(...) 何も起きずに通過する 失敗しても放置してよい副作用(計測など)
早期 return ユーザーにエラーを通知できる 失敗をユーザーに伝える必要がある

?. を足すか」ではなく「失敗を握り潰してよいか」が本来の論点だった。


ハマりどころ

括弧で囲むと短絡が切れる

短絡が効くのは1つのチェーン式の内側だけ。括弧でチェーンを閉じると、そこで評価が確定してしまう。

ts
const payload = undefined

payload?.successfulTags.forEach(f)   // ✅ undefined(短絡)
(payload?.successfulTags).forEach(f) // 💥 TypeError(括弧でチェーンが終わる)

変数に切り出すと短絡は引き継がれない

ts
const tags = payload?.successfulTags  // 型は Array<Tag> | undefined
tags.forEach(f)
//   ~~~~~~~ ❌ 'tags' は 'undefined' の可能性があります

実行時に落ちる前に TypeScript が止めてくれるが、?.式をまたいで効かない ことは意識しておく。

?. は「エラー」を握り潰す道具ではない

?. が表すのは「この値は存在しないことがある」という 仕様上の許容 であって、「たぶん大丈夫だと思うけど保険」ではない。保険で付けた ?. は、本当に守りたい箇所の ?. と見分けが付かなくなる。


まとめ

  1. a?.b.c()?.チェーン全体を短絡 する。a が nullish なら .c() には到達しない
  2. ?. が守るのは 左側の値だけb が nullable なら b?.c() が別途要る
  3. 要不要は推測ではなく 生成された型 / SDL の ! で判断する
  4. GraphQL の null 伝播により、Non-Null フィールドが失敗すると payload ごと null になる。型が実態と一致している
  5. 不要な ?. は将来の型エラーを潰す。@typescript-eslint/no-unnecessary-condition で機械的に弾ける
  6. 本当の論点は ?. の有無ではなく、失敗を握り潰してよいかどうか

参考

Index

  • optional chaining の短絡はチェーン全体に効く
  • 結論
  • 1. 題材:一括作成 mutation の結果を計測する
  • 2. ?. はチェーン全体を短絡する
  • 3. ?. が守るのは左側だけ
  • ?.() は「関数そのものが無い」ケース用
  • 4. では successfulTags は nullable なのか → 型を見る
  • 5. なぜ「payload はあるが successfulTags が null」が起きないのか
  • 6. 「念のため ?.」を足さない方がよい理由
  • lint で機械的に検出する
  • 7. 指摘のうち採用すべきだった部分
  • ハマりどころ
  • 括弧で囲むと短絡が切れる
  • 変数に切り出すと短絡は引き継がれない
  • ?. は「エラー」を握り潰す道具ではない
  • まとめ
  • 参考