順番に読み進めながら学べます

null と undefined — ?. と ?? で安全に読む

値が入っていない状態を表す null と undefined の違いを整理し、入れ子の途中が無くても止まらずに読む書き方と、既定値を安全に用意する書き方まで扱います。

サーバーから受け取った注文データを画面に出すとき、住所が未入力だったり、支払い方法がまだ選ばれていなかったりします。JavaScript にはこの「値が入っていない」状態を表す値が undefinednull の 2 つあり、読み取りに失敗すると TypeError で処理が止まります。

この記事では 2 つの違いと、途中が無くても止めずに読む書き方、無いときに既定値を当てる書き方を扱います。

値が入っていない 2 つの値 — undefined と null

undefined(値がまだ入っていないことを JavaScript 側が自動で入れる値)は、書き手が代入しなくても勝手に現れます。配列の範囲外やオブジェクトの無いキーを読んだときに undefined が返るのはすでに見たとおりで、これに変数の宣言だけをした場合が加わります。

  • let couponCode; のように、宣言だけして値を入れていない変数
  • order.customer のように、オブジェクトに登録されていないキー(既出)
  • tags[5] のように、要素が 2 つしかない配列の範囲外の index(既出)

一方の null(空であることを示すために書き手が代入する値)は、自動では現れません。「配送先はまだ選ばれていない」「クーポンは使われていない」のように、値が無いこと自体をデータとして持たせたいときに、自分で null を代入します。

API から返ってくる JSON でも、項目が空であることを示すために null が入っていることがあります。

2 つは別の値なので、=== で比べると false です。ただし == は両者を同じものとして扱うため true を返します。

判定を書くときは、どちらの値が来るのかを意識して === null のように片方だけを見るか、次の章以降で扱う ?.?? にまとめて任せます。

undefined と null は誰が入れるか
宣言だけの変数を読むJavaScript が自動で入れるundefined無いキー・範囲外のindex を読むJavaScript が自動で入れるundefined空であることを示したい書き手が代入するnull自動で入る自分で入れる
上の 2 行のように undefined は JavaScript が自動で入れる値、下の行のように null は書き手が代入する値 です。
let couponCode;                 // 宣言だけして値を入れていない
console.log(couponCode);        // undefined

const order = { orderId: "ORD-1105", total: 12800 };
console.log(order.customer);    // undefined(登録されていないキー)

const tags = ["限定", "予約受付"];
console.log(tags[5]);           // undefined(範囲外の index)

// null は書き手が代入する
let shippingAddress = null;     // 配送先はまだ選ばれていない
console.log(shippingAddress);   // null

console.log(shippingAddress === undefined);  // false(別の値)
console.log(shippingAddress == undefined);   // true(== は同じものとして扱う)

入れ子の途中が無くても止めない — オプショナルチェーン ?.

API から返ってくるデータは、項目がいつも全部そろっているとは限りません。order.customer.address.city のように深いところを読むコードは、途中の address が無い注文が 1 件混ざっただけで止まります。

読み取り自体を失敗させずに、無いときは無いまま先へ進めたい場面です。

order.customer.addressundefined のとき、その先の .city を読もうとすると TypeError: Cannot read properties of undefined (reading 'city') が発生します。1 段階目の読み取りは通っても、2 段階目で止まります。

そこで使うのが オプショナルチェーン(左側が nullundefined なら、その先を読まずに undefined を返す ?. 演算子)です。address?.city と書くと、address に値があるときだけ .city を読み、無いときは読み取りをやめて undefined を返します。

キーを [] で渡すときは ?.[field] の形で書きます。

?. がどこで読み取りをやめるか
customer値がある?.address値がある?.city福岡市shippingnull?.carrierここでやめるundefinedpaymentキーが無い?.methodここでやめるundefined
値があるところは先へ進み、null や undefined、キーそのものが無い場所に当たった時点で残りを読まずに undefined を返します
const order = {
  orderId: "ORD-1105",
  customer: { name: "中村 亮" },
};

// address が無いので、その先の city を読んだ時点で止まる
// console.log(order.customer.address.city);
// TypeError: Cannot read properties of undefined (reading 'city')

// ?. を挟むと、読み取りをやめて undefined が返る
console.log(order.customer.address?.city);   // undefined

// 値があるところはそのまま先へ進む
console.log(order.customer.name);            // 中村 亮

// キーを変数で渡すときは ?.[] と書く
const field = "city";
console.log(order.customer.address?.[field]);   // undefined

無くてもよい項目にだけ付ける

?. を全部の区切りに付けると、キー名の打ち間違いもエラーにならずに undefined として素通りします。必ず入っているはずの項目には付けず、入っていない可能性がある項目の手前だけ に付けると、直したいエラーは止めたまま読めます。

注文 API から返ってきた形のオブジェクトから、項目が欠けていても止まらずに値を読み取ります。response は宣言済みで、customer は必ず入っています。

① 配送先の市区町村を、途中が無くても止まらない書き方で表示してください。

② 配送業者名 shipping.carrier を、同じ書き方で表示してください。

③ 支払い方法 payment.method を、同じ書き方で表示してください。

④ 項目名を変数で受け取って、配送先の郵便番号を表示してください。

(正しく実行できれば解説が表示されます)

JavaScript / TypeScript エディタ

コードを実行してください

既定値を用意する — ?? と || の違い

項目が無いときは undefined のまま画面に出すのではなく、「ゲスト」「20 件」のような既定値を出したい場面がほとんどです。

ここで || を使うと、値が入っているのに既定値へ置き換わってしまうことがあります。在庫数の 0 や、備考の空文字列がその例です。

nullundefined の 2 つをまとめて nullish と呼びます。Nullish 合体演算子(左側が nullish のときだけ右側を返す ?? 演算子)を使うと、0 や空文字列や false はそのまま残ります。

|| は左側が falsy なら右側を返すので、値が無い場合と 0 の場合を区別できません。既定値を当てる目的なら ?? を選ぶと、意図しない置き換えを防げます。

?? と || で結果が分かれるのは falsy な値
stock は 0|| で判定falsy を置換20置き換わったstock は 0?? で判定null だけ置換0そのままstock は null?? で判定null だけ置換20置き換わった
在庫が 0 のときだけ結果が分かれます。?? は null と undefined だけを置き換える ので、0 は値として残ります。
左側の値|| が返すもの?? が返すもの
null既定値既定値
undefined既定値既定値
0既定値0 のまま
空文字列既定値空文字列のまま
false既定値false のまま
const settings = { displayName: null, itemsPerPage: 0 };

// ?? は左側が null か undefined のときだけ右側を返す
console.log(settings.displayName ?? "ゲスト");   // ゲスト
console.log(settings.itemsPerPage ?? 20);        // 0(そのまま残る)

// || は左側が falsy なら右側を返すので、0 も置き換わる
console.log(settings.itemsPerPage || 20);        // 20

// 登録されていないキーは、どちらでも既定値になる
console.log(settings.language ?? "ja");          // ja
console.log(settings.language || "ja");          // ja

なお displayName ?? "ゲスト" || "匿名" のように ??|| を括弧なしで並べると、どちらを先に判定するかが決まらないため文法エラーです。両方を使いたいときは (displayName ?? "ゲスト") || "匿名" のように括弧で区切ります。

ユーザー設定の各項目に既定値を当てて、画面に出す値を組み立てます。値が入っている項目はそのまま残す必要があります。userSettings は宣言済みです。

① 表示名に既定値「ゲスト」を当てて表示してください。

② 表示言語に既定値「en」を当てて表示してください。

③ 1 ページの表示件数に既定値 20 を当てて表示してください。

④ 登録されていない通知設定に既定値 false を当てて表示してください。

JavaScript / TypeScript エディタ

コードを実行してください

0 や空文字列を既定値で潰さない

在庫数 0・割引率 0・備考の空文字列は、どれも「値が入っている」状態です。|| で既定値を当てると、これらが falsy なので既定値に置き換わり、在庫切れの商品が在庫ありとして表示されるような食い違いが起きます。値の有無だけを見たいときは ?? を使います。

在庫 0 と空の備考を持つ商品で、既定値の当て方によって表示がどう変わるかを見比べます。product は宣言済みです。

① 在庫数に既定値 10 を || で当てて表示してください。

② 同じ在庫数に既定値 10 を ?? で当てて表示してください。

③ 備考に既定値「備考なし」を || で当て、角カッコで囲んで表示してください。

④ 同じ備考に既定値「備考なし」を ?? で当て、角カッコで囲んで表示してください。

JavaScript / TypeScript エディタ

コードを実行してください

登録されていない処理を呼ばない — ?.() の呼び出し

注文処理では、「成功したときの処理」「失敗したときの処理」をオブジェクトの中に入れておき、必要になった時点で呼び出すことがあります。中身が用意されていない項目まで呼び出すと TypeError: handlers.onError is not a function で止まるため、あるときだけ呼ぶ書き方が必要です。

第 3 回で扱った関数は、プロパティの値としてオブジェクトの中に入れておくこともできます。次の例の () => ... がその場で関数を作る記法です。この書き方そのものは文法カテゴリで扱うので、ここでは「プロパティに関数が入っているかどうかで結果が変わる」ところだけを見てください。

関数が入っているプロパティを handlers.onError?.() と書いて呼ぶと、onErrornullundefined のときは呼び出しを飛ばして undefined を返します。

?.() は登録が無ければ呼ばない
onSuccess は関数?.() で呼ぶ処理が実行されるonError はnull?.() で呼ぶ呼ばずにundefinedonError はnull?. を付けずに呼ぶTypeError で止まる
関数が入っていれば呼び出し、null なら呼ばずに undefined を返します。?. を付けずに呼ぶと TypeError で止まります
const handlers = {
  onSuccess: () => console.log("注文を確定しました"),
  onError: null,
};

// 関数が入っているので呼び出される
handlers.onSuccess?.();              // 注文を確定しました

// null なので呼び出しを飛ばし、undefined が返る
console.log(handlers.onError?.());   // undefined

// ?. を付けずに呼ぶと止まる
// handlers.onError();
// TypeError: handlers.onError is not a function

注文処理のハンドラをまとめたオブジェクトで、?. を付けた場合と付けない場合の違いを見比べます。handlers は宣言済みです。

① 成功時の処理を、登録されていないときは呼ばない書き方で呼び出してください。

② 失敗時の処理を同じ書き方で呼び出し、返ってきた値を表示してください。

③ 同じ失敗時の処理を ?. を付けずに呼び出して、どのメッセージで止まるかを確かめてください。

JavaScript / TypeScript エディタ

コードを実行してください
QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1let couponCode; と書いて値を入れずに読み取ったとき、変数の値は何ですか?

Q2?. の左側が nullundefined だったとき、その先はどうなりますか?

Q3const discountRate = 0; のとき、discountRate || 5discountRate ?? 5 はそれぞれ何を返しますか?