- JavaScript
JavaScriptの?.と??と||の違い|値別の結果一覧表とコピペ例
?.(オプショナルチェーン)は「左側が 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期連続の黒字決算。