?.(オプショナルチェーン)は「左側が null か undefined ならそこで止めて undefined を返す」演算子、??(Null合体演算子)は「左側が null か undefined のときだけ右側を使う」演算子、||(論理和)は「左側が falsy(0・空文字・false・NaN なども含む)なら右側を使う」演算子です。 3つの違いは「何を"値がない"とみなすか」の1点に集約できます。

この記事では、0・""・false・NaN・null・undefined などの値を実際に3つの演算子へ通し、Node.js 22.22 で実行した結果を一覧表にまとめました。|| で 0 や空文字が消えてしまう例の再現と ?? での直し方、?.() と ?.[]、代入演算子の ??=・||=・&&=、?? と || を括弧なしで混ぜたときの SyntaxError、右辺が評価されない短絡評価、TypeScript(tsc 6.0.3)での型の絞り込みまで、すべて実行ログつきで確認していきます。コードはそのままコピーして動かせます。

?.・??・|| の早見表

まず「どれを使うか」を決めるための表です。

演算子 名前 右側(または停止)に進む条件 主な用途 登場した仕様
a?.b オプショナルチェーン a が null / undefined あるかどうか分からないプロパティを安全に読む ES2020
a?.[k] オプショナルチェーン(ブラケット) a が null / undefined 変数キー・配列の添字で安全に読む ES2020
f?.() オプショナルチェーン(呼び出し) f が null / undefined あれば呼ぶ(コールバックなど) ES2020
a ?? b Null合体演算子 a が null / undefined 既定値(デフォルト値)を入れる ES2020
a || b 論理和 a が falsy(0・""・false・NaN・null・undefined・0n) 「空も含めて値がなければ」の既定値、条件のOR ES1
a && b 論理積 a が truthy のとき b を返す 条件が成り立つときだけ評価 ES1
a ??= b Null合体代入 a が null / undefined のときだけ代入 未設定の項目だけ埋める ES2021
a ||= b 論理和代入 a が falsy のときだけ代入 空の項目も含めて埋める ES2021
a &&= b 論理積代入 a が truthy のときだけ代入 値があるときだけ加工して上書き ES2021

値ごとの結果は次のとおりです(Node.js 22.22 で実行した結果)。|| と ?? で結果が分かれるのは 0・0n・""・false・NaN の5つです。

左側の値 v v || "既定" v ?? "既定" v?.length v.length(?. なし)
0 "既定" 0 undefined undefined
0n "既定" 0n undefined undefined
"" "既定" "" 0 0
false "既定" false undefined undefined
NaN "既定" NaN undefined undefined
null "既定" "既定" undefined TypeError
undefined "既定" "既定" undefined TypeError
"0" "0" "0" 1 1
[] [] [] 0 0
{} {} {} undefined undefined
"abc" "abc" "abc" 3 3
42 42 42 undefined undefined

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「JavaScriptの?.と??と||の違い|値別の結果一覧表とコピペ例」

3つの演算子の違いを30秒でつかむ

3つの演算子は、役割が少しずつ異なります。

知りたいこと 使う演算子 例
途中が null / undefined でもエラーにせず読みたい ?. user?.address?.city
値が「未設定」のときだけ既定値を入れたい ?? count ?? 10
0 や空文字も「値なし」とみなして既定値を入れたい || title || "無題"
読み取りと既定値をまとめて書きたい ?. と ?? user?.address?.city ?? "未登録"

?. は「読む・呼ぶ」ための演算子、?? と || は「代わりの値を選ぶ」ための演算子です。?. の結果は undefined になることがあるので、後ろに ?? を付けて既定値を入れる形がよく使われます。

?? と || の違いは「falsy」と「nullish」という2つの言葉で整理できます。

用語 意味 当てはまる値
falsy(フォルシー) 真偽値に変換すると false になる値 false、0、-0、0n、""、null、undefined、NaN
nullish(ヌリッシュ) 「値がない」ことを表す値 null、undefined の2つだけ

|| は falsy を、?? と ?. は nullish を判定に使います。nullish は falsy の一部なので、「?? は || より判定の範囲が狭い」と覚えると整理しやすくなります。

値別の結果を実行して確かめる

冒頭の表を出力したコードです。Node.js 14 以降であればそのまま動きます(ここでは Node.js 22.22.2 で実行しました)。

// table.mjs
const values = [0, 0n, "", false, NaN, null, undefined, "0", [], {}, "abc", 42];

const show = (v) =>
  typeof v === "string" ? JSON.stringify(v)
  : typeof v === "bigint" ? v + "n"
  : Array.isArray(v) ? "[]"
  : v && typeof v === "object" ? "{}"
  : String(v);

for (const v of values) {
  let chain, dot;
  try { chain = show(v?.length); } catch (e) { chain = e.name; }
  try { dot = show(v.length); } catch (e) { dot = e.name; }
  console.log(
    `${show(v).padEnd(10)} | ${show(v || "既定").padEnd(8)} | ` +
    `${show(v ?? "既定").padEnd(10)} | ${chain.padEnd(10)} | ${dot}`
  );
}

実行結果(node table.mjs):

0          | "既定"     | 0          | undefined  | undefined
0n         | "既定"     | 0n         | undefined  | undefined
""         | "既定"     | ""         | 0          | 0
false      | "既定"     | false      | undefined  | undefined
NaN        | "既定"     | NaN        | undefined  | undefined
null       | "既定"     | "既定"       | undefined  | TypeError
undefined  | "既定"     | "既定"       | undefined  | TypeError
"0"        | "0"      | "0"        | 1          | 1
[]         | []       | []         | 0          | 0
{}         | {}       | {}         | undefined  | undefined
"abc"      | "abc"    | "abc"      | 3          | 3
42         | 42       | 42         | undefined  | undefined

この結果から読み取れるポイントは4つあります。

  • || は 0・0n・""・false・NaN も既定値に置き換える(上から5行)。
  • ?? が既定値に置き換えるのは null と undefined の2行だけ。
  • 文字列の "0"、空配列 []、空オブジェクト {} は truthy なので、|| でも ?? でも置き換わりません。「空配列のときに既定値」としたい場合は arr?.length ? arr : 既定値 のように長さで判定します。
  • ?. がエラーを防ぐのは null と undefined の2行だけ。数値や真偽値は ?. がなくてもプロパティ読み取りでエラーにならず、undefined が返ります。

NaN は ?? では置き換わらない点も押さえておくと役立ちます。Number("abc") ?? 0 は NaN のままです。数値変換の結果に既定値を入れるときは、Number.isNaN(n) ? 0 : n のように NaN を明示的に判定します。

|| で 0 や空文字が消える例と ?? での直し方

|| を既定値に使うと、「0」「空文字」「false」という正しい値まで既定値に置き換わることがあります。在庫数 0、メモが空、通知オフという設定を表示する例で再現します。

直す前(|| を使う)

// bug.mjs(前半)
function renderBefore(item) {
  const stock = item.stock || "在庫数不明";
  const memo = item.memo || "(メモなし)";
  const notify = item.notify || true;
  return { stock, memo, notify };
}

const item = { name: "ノート", stock: 0, memo: "", notify: false };
console.log("直す前:", renderBefore(item));

実行結果:

直す前: { stock: '在庫数不明', memo: '(メモなし)', notify: true }

在庫 0 が「在庫数不明」に、通知オフ(false)が true に変わりました。入力データは正しいのに、表示だけが変わってしまう形です。notify || true は左側が何であっても必ず true になるため、利用者がオフにした設定が反映されません。

直した後(?? を使う)

// bug.mjs(後半)
function renderAfter(item) {
  const stock = item.stock ?? "在庫数不明";
  const memo = item.memo ?? "(メモなし)";
  const notify = item.notify ?? true;
  return { stock, memo, notify };
}

console.log("直した後:", renderAfter(item));
console.log("未設定:", renderAfter({ name: "ペン" }));

実行結果:

直した後: { stock: 0, memo: '', notify: false }
未設定: { stock: '在庫数不明', memo: '(メモなし)', notify: true }

?? に変えると、0・""・false はそのまま残り、プロパティ自体がない(undefined)ときだけ既定値が入ります。

どちらを選ぶかの判断表

|| が適している場面もあります。「空文字のときも既定の表示にしたい」というケースです。

場面 0・""・false の扱い 選ぶ演算子 例
数量・件数・金額・ページ番号・インデックス 0 は意味のある値 ?? page ?? 1
ON/OFF の設定値 false は意味のある値 ?? enabled ?? true
入力欄の値(空欄を許す) "" は意味のある値 ?? memo ?? ""
表示名(空欄なら代わりの文言) "" は「なし」と同じ扱いでよい || nickname || "ゲスト"
環境変数(未設定も空文字も既定値にしたい) "" は「なし」と同じ扱いでよい || process.env.PORT || "3000"
真偽の条件を OR でつなぐ 真偽値として評価 || isAdmin || isOwner

迷ったら「0 や空文字が来たときに、それを残したいか」を自分に問いかけると決めやすくなります。残したいなら ??、既定値に置き換えたいなら || です。

関数の既定引数との違い

関数の引数に既定値を付ける書き方(function f(x = 10))は、undefined のときだけ既定値が使われます。null では使われない点が ?? と異なります。

渡す値 function f(x = 10) の x x ?? 10 x || 10
undefined(省略) 10 10 10
null null 10 10
0 0 0 10
"" "" "" 10

API のレスポンスなどで null が入ってくる可能性があるなら、既定引数だけに頼らず関数の中で ?? を使うと、null と undefined の両方をまとめて扱えます。分割代入の既定値(const { x = 10 } = obj)も既定引数と同じく undefined のときだけ働きます。

?. の3つの書き方(プロパティ・?.[]・?.())

?. には3つの形があります。まとめて動かして確認します。

// chain.mjs
const user = {
  name: "sato",
  address: { city: "Tokyo" },
  tags: ["admin", "dev"],
  greet() { return `hi, ${this.name}`; },
};
const guest = null;

console.log("1", user?.address?.city);   // プロパティ
console.log("2", guest?.address?.city);  // guest が null
console.log("3", user.profile?.bio);     // profile がない
console.log("4", user?.tags?.[0]);       // 配列の添字
console.log("5", guest?.tags?.[0]);
console.log("6", user.greet?.());        // あるメソッドを呼ぶ
console.log("7", user.logout?.());       // ないメソッドは呼ばない
console.log("8", guest?.greet());        // guest が null なら呼ばない
const key = "city";
console.log("9", user.address?.[key]);   // 変数キー

実行結果:

1 Tokyo
2 undefined
3 undefined
4 admin
5 undefined
6 hi, sato
7 undefined
8 undefined
9 Tokyo
形 書き方 意味 注意点
プロパティ a?.b a が nullish なら undefined、そうでなければ a.b もっともよく使う形
ブラケット a?.[expr] a が nullish なら undefined、そうでなければ a[expr] ?. と [ の間にドットが入る(a?[0] ではない)
呼び出し f?.(args) f が nullish なら undefined、そうでなければ f(args) f?.() とドットが入る(f?() ではない)

ブラケットと呼び出しの形は、?. の後ろに [ や ( を続けるのがポイントです。a?[0] と書くと、JavaScript は条件演算子(a ? [0] : ...)の始まりと解釈しようとするため、構文エラーになります。

?. がない場合に出るエラー

同じデータに対して ?. を付けずに読むと、次のエラーになります。

try { console.log(guest.address.city); } catch (e) { console.log("10", e.name + ": " + e.message); }
try { console.log(user.profile.bio); }   catch (e) { console.log("11", e.name + ": " + e.message); }
try { user.logout(); }                   catch (e) { console.log("12", e.name + ": " + e.message); }

実行結果:

10 TypeError: Cannot read properties of null (reading 'address')
11 TypeError: Cannot read properties of undefined (reading 'bio')
12 TypeError: user.logout is not a function

Cannot read properties of undefined (reading 'xxx') というエラーは、reading の後ろに書かれたプロパティの「1つ手前」が undefined だったことを示しています。11 の例なら bio を読もうとした user.profile が undefined です。この手前の位置に ?. を置くと、エラーではなく undefined が返るようになります。

?. が止めてくれる範囲

?. は、左側が nullish だった時点でそのチェーンの残り全体を飛ばします。

console.log("15", guest?.a.b.c.d);
try { console.log((guest?.a).b); } catch (e) { console.log("16", e.name + ": " + e.message); }

実行結果:

15 undefined
16 TypeError: Cannot read properties of undefined (reading 'b')

guest?.a.b.c.d は、guest が null と分かった時点で .a.b.c.d 全体を評価せずに undefined を返します。一方、括弧で (guest?.a) と区切るとチェーンがそこで終わるため、括弧の結果(undefined)に対して .b を読むことになり、エラーになります。

ただし「guest?.a.b」という書き方は、guest が存在して a が undefined の場合にはエラーになります。?. が守るのは、すぐ左側の値だけです。途中のどこが欠ける可能性があるかを考えて、欠ける可能性がある場所の直後に ?. を置きます。

書き方 obj が null obj.a が undefined obj.a.b が undefined
obj.a.b.c TypeError TypeError undefined
obj?.a.b.c undefined TypeError undefined
obj?.a?.b.c undefined undefined undefined
obj?.a?.b?.c undefined undefined undefined

?. が守らないケース

?. を付けていてもエラーになる場面が3つあります。実行して確認しました。

// 関数ではない値を呼ぶ
try { user.name?.(); }   catch (e) { console.log("13", e.name + ": " + e.message); }
// 宣言していない変数
try { notDeclared?.x; }  catch (e) { console.log("14", e.name + ": " + e.message); }

実行結果:

13 TypeError: user.name is not a function
14 ReferenceError: notDeclared is not defined
ケース 結果 対処
値はあるが関数ではないものを ?.() で呼ぶ TypeError(is not a function) typeof f === "function" で判定してから呼ぶ
宣言されていない変数に ?. を付ける ReferenceError typeof x !== "undefined" で確認、または globalThis.x?.y と書く
a?.b.c の b が undefined TypeError a?.b?.c と、欠ける可能性のある場所にも ?. を置く

?. を書けない場所

次の3つは文法上書けません。Node.js 22.22 で実行したときのエラー文です。

書いたコード エラー文 代わりの書き方
o?.a = 1 SyntaxError: Invalid left-hand side in assignment if (o) o.a = 1;
new Intl?.DateTimeFormat() SyntaxError: Invalid optional chain from new expression Intl ? new Intl.DateTimeFormat() : undefined
String?.raw`a` SyntaxError: Invalid tagged template on optional chain 事前に存在を確認してから使う

?. は「読む」「呼ぶ」「delete する」ための演算子で、代入の左辺には置けません。 なお delete guest?.x は書くことができ、guest が null のときは何もせず true を返します(実行して確認しました)。

よく使う組み合わせ

配列メソッドや Map、正規表現など、「見つからないと undefined や null を返す」メソッドと ?. は相性が良好です。

// Map.get は見つからないと undefined
const m = new Map([["a", { n: 1 }]]);
console.log("20", m.get("a")?.n, m.get("z")?.n);

// find は見つからないと undefined
console.log("21", [1, 2, 3].find((x) => x > 5)?.toFixed(1));

// match は一致しないと null
console.log("22", "2026-10-02".match(/(\d+)-(\d+)/)?.[1], "abc".match(/(\d+)/)?.[1]);

実行結果:

20 1 undefined
21 undefined
22 2026 undefined
やりたいこと コード
DOM 要素があれば値を読む document.querySelector("#name")?.value ?? ""
要素があればイベントを付ける document.querySelector("#btn")?.addEventListener("click", onClick)
コールバックが渡されていれば呼ぶ options.onDone?.(result)
API レスポンスの深い階層を読む res.data?.items?.[0]?.title ?? "(なし)"
配列の件数を安全に得る list?.length ?? 0
正規表現の一致結果を取り出す text.match(/id=(\d+)/)?.[1]
JSON の任意項目を読む config.server?.port ?? 3000

?. と ?? を組み合わせて既定値を入れる

?. の結果は undefined になり得るので、表示や計算に使う前に ?? で既定値を入れるのが定番の形です。

console.log("18", guest?.address?.city ?? "未登録");
console.log("19", user.tags?.length ?? 0, guest?.tags?.length ?? 0);

実行結果:

18 未登録
19 2 0

ここで ?? の代わりに || を使うと、user.tags?.length が 0(空配列)のときにも既定値へ置き換わります。件数のように 0 が意味を持つ値では ?? を選びます。

?. と ?? は優先順位の面でも素直に組み合わせられます。?. はプロパティアクセスと同じ高い優先順位、?? は || と同じ低い優先順位なので、a?.b ?? c は (a?.b) ?? c として評価されます。

短絡評価:右辺は必要なときだけ評価される

??・||・&&・?. はいずれも短絡評価(ショートサーキット)を行います。左側だけで結果が決まるとき、右側の式は評価されません。右側に関数呼び出しを置いて、呼ばれたかどうかを確認します。

// short.mjs
let calls = 0;
const heavy = (label) => {
  calls++;
  console.log(`  heavy() が呼ばれた: ${label}`);
  return "既定";
};

console.log("A:", "値あり" ?? heavy("A"));
console.log("B:", null ?? heavy("B"));
console.log("C:", 0 || heavy("C"));
console.log("D:", 0 ?? heavy("D"));
console.log("E:", 1 && heavy("E"));
console.log("F:", 0 && heavy("F"));

const obj = null;
let i = 0;
console.log("G:", obj?.[i++], obj?.method(heavy("G")), "i =", i);
console.log("heavy の呼び出し回数:", calls);

実行結果:

A: 値あり
  heavy() が呼ばれた: B
B: 既定
  heavy() が呼ばれた: C
C: 既定
D: 0
  heavy() が呼ばれた: E
E: 既定
F: 0
G: undefined undefined i = 0
heavy の呼び出し回数: 3
ケース 式 右辺は評価されたか 理由
A "値あり" ?? heavy() されない 左側が nullish ではない
B null ?? heavy() される 左側が null
C 0 || heavy() される 0 は falsy
D 0 ?? heavy() されない 0 は nullish ではない
E 1 && heavy() される 1 は truthy
F 0 && heavy() されない 0 は falsy
G obj?.[i++]、obj?.method(heavy()) されない obj が null。添字の i++ も引数の heavy() も評価されない

G の結果で i が 0 のままである点が重要です。 ?. で止まった場合、ブラケットの中の式や関数の引数も評価されません。右辺に「カウントを増やす」「ログを書く」といった副作用のある式を置くと、左側の値によって動いたり動かなかったりします。副作用のある処理は別の文に分けておくと、読み手にとって動きが追いやすくなります。

短絡評価は、重い処理を必要なときだけ実行する用途にも使えます。

// キャッシュにあればそれを使い、なければ計算する(計算は必要なときだけ)
const result = cache.get(key) ?? computeExpensive(key);

??=・||=・&&= の違い(論理代入演算子)

ES2021 で追加された3つの代入演算子は、「条件を満たすときだけ代入する」短い書き方です。

演算子 意味 同じ意味の書き方
a ??= b a が null / undefined のときだけ b を代入 a ?? (a = b)
a ||= b a が falsy のときだけ b を代入 a || (a = b)
a &&= b a が truthy のときだけ b を代入 a && (a = b)

値ごとの結果を console.table で一覧にしました。

// assign.mjs(前半)
const base = () => ({ a: 0, b: "", c: null, d: undefined, e: "x", f: false });
const rows = [];
for (const k of [..."abcdef", "g"]) {   // g は存在しないプロパティ
  const o1 = base(), o2 = base(), o3 = base();
  o1[k] ||= "NEW";
  o2[k] ??= "NEW";
  o3[k] &&= "NEW";
  const s = (v) => JSON.stringify(v) ?? "undefined";
  rows.push({ 元の値: s(base()[k]), "||=": s(o1[k]), "??=": s(o2[k]), "&&=": s(o3[k]) });
}
console.table(rows);

実行結果を表に整理すると次のとおりです。

元の値 ||= "NEW" の後 ??= "NEW" の後 &&= "NEW" の後
0 "NEW" 0 0
"" "NEW" "" ""
null "NEW" "NEW" null
undefined "NEW" "NEW" undefined
"x" "x" "x" "NEW"
false "NEW" false false
(プロパティなし) "NEW" "NEW" undefined

??= は「まだ設定されていない項目だけを埋める」、||= は「空の項目も含めて埋める」、&&= は「値がある項目だけを上書きする」と覚えると使い分けやすくなります。

代入が起きないときは setter も呼ばれない

a ??= b と a = a ?? b は結果の値こそ同じですが、代入が行われるかどうかが異なります。setter(代入時に呼ばれる関数)を持つオブジェクトで確認します。

// assign.mjs(中盤)
const t = {
  _v: 1,
  get v() { return this._v; },
  set v(x) { console.log("  setter 呼び出し", x); this._v = x; },
};
t.v ??= 99;      console.log("??= 後:", t.v);
t.v = t.v ?? 99; console.log("= ?? 後:", t.v);

実行結果:

??= 後: 1
  setter 呼び出し 1
= ?? 後: 1

t.v ??= 99 では setter が呼ばれず、t.v = t.v ?? 99 では同じ値 1 で setter が呼ばれました。??= は条件を満たさないとき代入そのものを行いません。 値の変更を監視する仕組み(リアクティブなフレームワークの状態オブジェクトや Proxy など)では、不要な更新通知を避けられるという利点になります。

??= のよく使う形

// assign.mjs(後半)
// 1) 一度作ったオブジェクトを使い回す(簡易キャッシュ)
const cache = {};
function getUser(id) {
  return (cache[id] ??= { id, loadedAt: "now" });
}
console.log(getUser(1) === getUser(1));

// 2) 出現回数を数える
const counts = {};
for (const w of ["a", "b", "a"]) {
  counts[w] ??= 0;
  counts[w]++;
}
console.log(counts);

// 3) オプションの未設定項目だけ既定値で埋める
const opts = { retries: 0 };
opts.retries ??= 3;
opts.timeout ??= 5000;
console.log(opts);

実行結果:

true
{ a: 2, b: 1 }
{ retries: 0, timeout: 5000 }

3 の例では、retries: 0(リトライしない)という指定がそのまま残り、未設定の timeout だけが埋まっています。ここを ||= にすると retries が 3 に置き換わります。

やりたいこと コード
未設定の項目だけ既定値で埋める opts.timeout ??= 5000
配列がなければ作ってから追加する (groups[key] ??= []).push(item)
カウンターを初期化してから増やす counts[k] ??= 0; counts[k]++
空文字のときも既定の文言を入れる user.displayName ||= "ゲスト"
値があるときだけ整形して上書きする user.email &&= user.email.trim().toLowerCase()

?? と || を括弧なしで混ぜると SyntaxError になる

?? は、|| や && と括弧なしで同じ式に並べることができません。実行前の構文解析の段階でエラーになります。Node.js 22.22 での結果です。

const v = null || undefined ?? "x";
const v = null || undefined ?? "x";
                            ^^
SyntaxError: Unexpected token '??'

順序を入れ替えた場合や && と混ぜた場合も同様です。

書いたコード Node.js 22.22 のエラー文
const v = null || undefined ?? "x"; SyntaxError: Unexpected token '??'
const v = true && undefined ?? "x"; SyntaxError: Unexpected token '??'
const v = null ?? undefined || "x"; SyntaxError: Unexpected token '||'

エラー文は書く場所によって変わります。同じ式を console.log(...) の引数の中に書いた場合は、SyntaxError: missing ) after argument list と表示されました。エラー文に ?? という文字が出てこなくても、キャレット(^^)が指している位置が || や ?? の近くなら、この混在を疑うと原因にたどり着きやすくなります。

括弧を付けて直す

どちらを先に評価したいかを括弧で示せば動きます。

console.log((null || undefined) ?? "x");  // x
console.log(null || (undefined ?? "x"));  // x

なぜ括弧が必須なのかというと、括弧の位置によって結果が変わる組み合わせがあるからです。a ?? b || c で a = 0, b = 1, c = 2 の場合を実行して比べました。

const a = 0, b = 1, c = 2;
console.log((a ?? b) || c);  // 2(a ?? b は 0。0 は falsy なので c)
console.log(a ?? (b || c));  // 0(a は nullish ではないので a)

読み間違いを防ぐために、仕様として評価順の明示を求めています。括弧で意図を書いておくことが、そのまま読み手への説明になります。

TypeScript でも同じ混在はコンパイルエラーになります(後述の TS5076)。

TypeScript での型の絞り込み

TypeScript では、?. と ?? が型にも反映されます。tsc 6.0.3(--strict)で確認しました。

?. の結果は「| undefined」が付く

// narrow.ts
type Address = { city: string; zip?: string };
type User = {
  name: string;
  address?: Address | null;
  tags?: string[];
  onSave?: (id: number) => void;
};

function cityLength(user: User | undefined): number {
  // ?. の結果は string | undefined
  const city = user?.address?.city;

  // ?? で undefined を取り除くと string になる
  const safe = user?.address?.city ?? "未登録";
  return safe.length;
}

user?.address?.city の型は string | undefined です。そのまま .length を読むとコンパイルエラーになり、?? "未登録" を付けると string に絞り込まれます。エラーになる書き方を tsc に通した結果です。

// narrow_err.ts
type User = { name: string; address?: { city: string } | null };

function f(user: User | undefined) {
  const city = user?.address?.city;
  return city.length;           // 4行目
}
function g(user: User | undefined) {
  return user.address.city;     // 7行目
}
function h(n: number) {
  const a = n ?? 0;
  const b = { x: 1 } ?? 2;      // 11行目
  return [a, b];
}
const mixed = (a: string | null, b: string | undefined) => a || b ?? "x";  // 14行目

実行結果(tsc --strict --target es2022 --noEmit narrow_err.ts):

narrow_err.ts(4,10): error TS18048: 'city' is possibly 'undefined'.
narrow_err.ts(7,10): error TS18048: 'user' is possibly 'undefined'.
narrow_err.ts(7,10): error TS18049: 'user.address' is possibly 'null' or 'undefined'.
narrow_err.ts(11,13): error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
narrow_err.ts(14,60): error TS5076: '||' and '??' operations cannot be mixed without parentheses.
エラーコード 意味 直し方
TS18048 値が undefined かもしれない ?. で読む、?? で既定値を入れる、if で確認する
TS18049 値が null または undefined かもしれない 同上
TS2869 ?? の左側が nullish になり得ないため、右側は使われない 不要な ?? を外す
TS5076 || と ?? を括弧なしで混ぜている 括弧で評価順を示す

TS18048 や TS18049 が出た場所は、JavaScript として実行したときに Cannot read properties of undefined が起こり得る場所です。コンパイルの段階で ?. を置くべき位置を教えてくれている、と受け取ると活用しやすくなります。

?. を条件に使うと、ブロック内で型が絞り込まれる

function narrow(user: User | undefined) {
  // ?. を条件に使うと、ブロック内では user も address も「存在する」型になる
  if (user?.address?.city) {
    const a: Address = user.address;      // Address | null | undefined ではなく Address
    console.log(a.city.toUpperCase());
  }
  // === での比較でも絞り込める
  if (user?.name === "sato") {
    console.log(user.name);               // user は User
  }
  // !== undefined の比較でも絞り込める
  if (user?.tags?.length !== undefined) {
    console.log(user.tags.length);        // user.tags は string[]
  }
  user?.onSave?.(1);
}

このコードは tsc --strict でエラーなし(終了コード 0)でコンパイルできました。if (user?.address?.city) が真になるのは、user も address も存在する場合だけなので、TypeScript はブロックの中で user.address を Address 型として扱います。ブロック内で ?. を何度も書く必要がなくなります。

?? と ??= による絞り込み

function defaults(input: { retries?: number; label?: string | null }) {
  const retries = input.retries ?? 3;   // number
  const label = input.label ?? "none";  // string(null も undefined も取り除かれる)
  const r2 = input.retries || 3;        // number(0 は 3 に置き換わる)
  input.retries ??= 3;
  const n: number = input.retries;      // ??= の後は number に絞り込まれる
  return { retries, label, r2, n };
}
console.log(defaults({ retries: 0, label: null }));

実行結果(コンパイル後に node narrow.js):

{ retries: 0, label: 'none', r2: 3, n: 0 }

?? も || も型の上では number になります。型は同じでも、retries: 0 を渡したときの値は ?? が 0、|| が 3 と異なります。 型チェックでは見分けられない違いなので、値別の表で確認しておくことが大切です。

コンパイル先(target)による出力の違い

?. と ?? は ES2020、??= などは ES2021 の構文です。tsc の --target をそれより前にすると、同じ意味の式に変換されます。次の1行を各 target でコンパイルした結果です。

const u: any = null;
console.log(u?.a?.[0] ?? "d");
const o: any = {};
o.x ??= 1; o.y ||= 2; o.x &&= 5;
target u?.a?.[0] ?? "d" の出力 o.x ??= 1 の出力
es2019 (_b = (_a = u === null || u === void 0 ? void 0 : u.a) === null || ...) !== null && _b !== void 0 ? _b : "d" (_c = o.x) !== null && _c !== void 0 ? _c : (o.x = 1)
es2020 u?.a?.[0] ?? "d"(そのまま) o.x ?? (o.x = 1)
es2021 以降 u?.a?.[0] ?? "d"(そのまま) o.x ??= 1(そのまま)

es2019 向けの出力を見ると、?. と ?? が「=== null || === void 0(null または undefined か)」という判定の省略形であることがよく分かります。

対応環境

構文 仕様 Node.js 補足
?.、?? ES2020 14 以降 現行の主要ブラウザーはすべて対応
??=、||=、&&= ES2021 15 以降 現行の主要ブラウザーはすべて対応

現在サポートされている Node.js(20 以降)と主要ブラウザーの現行版では、いずれも追加設定なしで使えます。より前の実行環境に配布する場合は、TypeScript の target や Babel などのトランスパイラーで変換できます。ブラウザーごとの対応バージョンは MDN の「ブラウザーの互換性」の表で確認できます。

使い分けチェックリスト

コードを書くとき・レビューするときに使える確認項目です。

確認すること 目安
既定値を入れる対象に 0・""・false が来る可能性があるか あるなら ??
空文字も「値なし」と同じ扱いにしたいか そうなら ||
x || true や x || 1 のように、右辺が truthy 固定になっていないか false や 0 を残したいなら ??
?. を付ける位置は「欠ける可能性のある値」の直後か a?.b.c は a だけを守る
すべてのドットを ?. にしていないか 必ず存在する値には . を使うと、想定外の欠落に早く気づける
?. の結果をそのまま計算・表示に使っていないか ?? 既定値 を添える
?? と ||・&& を同じ式で使うとき括弧を付けたか 括弧なしは SyntaxError
右辺に副作用のある式を置いていないか 短絡評価で実行されない場合がある
NaN を既定値に置き換えたいか ?? では置き換わらない。Number.isNaN で判定

「すべてのドットを ?. にする」書き方は、エラーは出なくなる一方で、本来あるはずの値が欠けていたことに気づきにくくなります。 「ここは欠けることがある」と分かっている場所にだけ ?. を置くと、コードが「どこが任意の項目か」を伝える資料にもなります。

よくある質問

Q. ?? と || はどちらを基本にすればよいですか? A. 既定値を入れる用途では ?? を基本にすると、0・空文字・false がそのまま残るので意図どおりになりやすいです。「空文字のときも既定の文言にしたい」「真偽の条件を OR でつなぎたい」という場面では || が適しています。

Q. a?.b と a && a.b は同じですか? A. 似ていますが結果が異なる場合があります。a が 0 や "" のとき、a && a.b は 0 や "" をそのまま返し、a?.b は (0).b や "".b を読んで undefined を返します。また a が null のとき、a && a.b は null、a?.b は undefined を返します。?. は「null か undefined か」だけを判定する点が違いです。

Q. ?. を使ったのに Cannot read properties of undefined が出ます。 A. ?. が守るのはすぐ左側の値だけです。a?.b.c で b が undefined なら、.c の読み取りでエラーになります。エラー文の reading 'xxx' に書かれたプロパティの1つ手前に ?. を追加してください(例:a?.b?.c)。括弧で (a?.b).c と区切った場合もチェーンが切れるため、同じエラーになります。

Q. ??= と、関数の既定引数(x = 10)はどう違いますか? A. 既定引数は undefined のときだけ働き、null が渡された場合は null のままです。??= と ?? は null と undefined の両方で働きます。外部から null が入ってくる可能性があるデータには ?? が向いています。

まとめ

  • ?. は「左が null / undefined なら止めて undefined を返す」、?? は「左が null / undefined のときだけ右を使う」、|| は「左が falsy なら右を使う」。
  • || と ?? で結果が分かれるのは 0・0n・""・false・NaN の5つ。数量・設定値・入力値の既定値には ?? が合う。
  • ?. には a?.b・a?.[k]・f?.() の3つの形があり、守るのはすぐ左側の値だけ。代入の左辺には書けない。
  • ?. と ?? は a?.b ?? 既定値 の形で組み合わせると、読み取りと既定値を1行で書ける。
  • ??= は未設定の項目だけ、||= は空の項目も、&&= は値がある項目だけを代入する。条件を満たさないときは代入自体が起きない。
  • ?? と ||・&& を括弧なしで混ぜると SyntaxError(TypeScript では TS5076)。括弧で評価順を示す。
  • 右辺は必要なときだけ評価される(短絡評価)。TypeScript では ?. と ?? が型の絞り込みにも反映される。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「JavaScriptの?.と??と||の違い|値別の結果一覧表とコピペ例」

株式会社RJCでは、JavaScriptの演算子の細かな違いや TypeScript の型の扱いを仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?