TL;DR
- オブジェクトが未定義の場合の値代入では、モダンな Null合体演算子 である
??
を使った方が可読性も高くバグが少ない - ECMAScript ではそのほかにも便利な機能が増えている
はじめに
こんにちは。みなさんは最近、オブジェクトが null のときに代わりの値をどのように代入していますか?
私自身、三項演算子を使ったワンライナーの実装で満足していた節があるのですが、TypeScript などの言語では Null合体演算子 なるオペレータがあることを認識しました。認識したというのは、フレームワークのチュートリアルなどをコピー&ペーストしてコーディングするときには、それらしい記号を目撃しつつも、ろくに仕様を調べないまま使っていました。あらためてドキュメントなどで仕様を確認することで便利さがわかったため、この機会にまとめてみようと思います。演算子を身につけて適切にコーディングすることで可読性の高くバグの少ないコーディングができるようになればと思います。
Null合体演算子; Nullish coalescing operator (??
)
変数や関数を複数並べて記載にしてその間に ??
の演算子記号をおくことで実装します。??
を置くことで、??
の左側の変数 or 関数(オペランド)に値を持っている場合はそれを返しますが、null か undefined の場合は、右側のオペランドを返します。この Null合体演算子 を使うことで、変数の代入文で代入元が未定義の場合のデフォルト値を設定する実装を簡潔に行うことができます。
let value1: string | null = null;
let value2: string | undefined = undefined;
let value3: string | null = "Hello, TypeScript!";
// null または undefined の場合にデフォルト値を設定
let result1 = value1 ?? "Default Value"; // "Default Value"
let result2 = value2 ?? "Default Value"; // "Default Value"
let result3 = value3 ?? "Default Value"; // "Hello, TypeScript!"
console.log(result1); // "Default Value"
console.log(result2); // "Default Value"
console.log(result3); // "Hello, TypeScript!"
TypeScript ではこの実装を、null または undefined を処理するときにデフォルト値を「フォールバック」するとしています。||
演算子と似ていますが、??
は null と undefined だけを「falsy」と見なすため、0 や空文字列("")などの他のfalsyな値には反応しません(リリースノート内容)。
上記コードは、TypeScript 公式が提供するプレイグラウンドで試すことができます。
Null合体演算子 は ECMAScript2020(ES11) で初めて導入されました。2020 となっていますが、2019年に最終化されています。そして2019年リリースの TypeScript3.7 でサポートされました。
何が嬉しいのか?
let result2 = value2 ?? "Default Value";
と実装すると、value2
が存在する場合はそれが使用され、null または undefined の場合は代わりに "Default Value" が代入されます。これは三項演算子を使った次の以下の実装と同等です。
let result2 = value2 !== null && value2 !== undefined ? value2 : "Default Value";
さらにはif文を使った以下の実装と同等です。宣言文を含め5行分の処理を簡潔に表現できることがわかりますでしょうか?
let result2: string;
if (value2 !== null && value2 !== undefined) {
result2 = value2;
} else {
result2 = "Default Value";
}
論理和演算子 ||
との比較
類似の実装として、デフォルト値の実装に論理和(||
)演算子を使用することができます。ただ、||
は falsy な値( false、0、""、NaN、null、undefined)をすべて偽として扱います。このことで、??
よりも意図しないデフォルト値の適用が発生しがちです。
let discount = inputDiscount || 100;
price = 0
は falsy な値ですが、意図としては 0 をそのまま価格として扱いたい場合もあります。しかし、||
を使うと 0 は「falsy」として扱われ、デフォルト値 100 が適用されてしまいます。
本来 0 は「有効な値」ですが、||
では 0 を無効として扱ってしまいます。
let loginUser = name || "Guest";
name = ""
は falsy な値ですが、空文字列を渡した場合でもデフォルト値 "Guest" が適用されてしまいます。
空文字列("")は有効な「名前」として扱いたいケースもありますが、||
を使うと空文字列が無効として扱われ、デフォルト値 "Guest" が返されます。
??
を使うと、null または undefined のみがデフォルト値を適用される条件となります。これにより、0 や空文字列("")が有効な値として扱われ、意図しないデフォルト値の適用を防げます。
let discount = inputDiscount ?? 100;
let loginUser = name ?? "Guest";
||
は、ECMAScript3(1999年) で最初に導入されました。TypeScript および JavaScript の基本的な論理演算子として、真偽値の比較に広く使われています。||
演算子は、左側が falsy(false、0、""、null、undefined、NaN)であれば右側の値を返します。
ただ、上記の通りfalse、0、""、は値が設定されていると認識されないため、デフォルト値の設定には、後発のNull合体演算子 ??
の方が適切な場面もあるかもしれません。
まとめ
最後に、次の式で変数に格納されるのが左辺の値なのか 右 辺の値なのか、表にまとめてみます。
let 変数 = [左辺の値] [ ?? or || ] [右辺の値]
# | 左辺のとる値 | Null合体演算子 ??
|
論理和演算子 ||
|
---|---|---|---|
1 | null | 右 辺の値 | 右 辺の値 |
2 | undefined | 右 辺の値 | 右 辺の値 |
3 | 0 | 左辺の値 | 右 辺の値 |
4 | false | 左辺の値 | 右 辺の値 |
5 | "" | 左辺の値 | 右 辺の値 |
6 | 上記以外 | 左辺の値 | 左辺の値 |
いかがでしたでしょうか?タイトルの通りですが、デフォルト値の設定に Null合体演算子 が使える場合は、使いましょう。またその際、代入する値が実装したい仕様にあっているかどうかを注意しましょう。
参考
ECMAScript における Null合体演算子 の仕様
TypeScript3.7 のリリースノート
TypeScript のドキュメント
TypeScript のプレイグラウンド
付録
Null合体演算子 以外にも ECMAScript で比較的新しい演算子をまとめておきます。
-
?.
(Optional Chaining Operator): null や undefined を安全にチェーンしてアクセスする演算子 - Logical Assignment Operators (
&&=
,||=
,??=
): 論理演算と代入を組み合わせた新しい演算子 - Private Fields and Methods (
#
): クラスのプライベートフィールドとメソッドを定義するための新しい構文 - Top-Level
await
: モジュールのトップレベルでawait
を直接使用できるようになった機能 -
globalThis
: 環境に依存せず、グローバルオブジェクトにアクセスするための新しい方法
Optional Chaining Operator (?.
)
- 導入
- ECMAScript 2020(ES11)
- TypeScript サポート
- TypeScript 3.7(2019年11月)
?.
(オプショナルチェイニング演算子)は、オブジェクトが null または undefined であるかをチェックし、その後にプロパティやメソッドにアクセスすることを簡単にする演算子です。これにより、null や undefined でのアクセスエラーを防ぐことができます。
let user = { name: "John", address: { city: "New York" } };
// address が null または undefined の場合でもエラーにならない
let city = user.address?.city; // "New York"
let country = user.address?.country; // undefined(エラーなし)
console.log(city); // "New York"
console.log(country); // undefined
user.address?.city
のように、address
が存在しない場合でもエラーを出さずに undefined を返します。
これにより、null や undefined チェックを手動で行う必要がなくなり、コードがシンプルになります。
Logical Assignment Operators (&&=
, ||=
, ??=
)
- 導入
- ECMAScript 2021(ES12)
- TypeScript サポート
- TypeScript 4.4(2021年8月)
-
&&=
: 左辺が truthy であれば右辺を代入 -
||=
: 左辺が falsy であれば右辺を代入 -
??=
: 左辺が null または undefined であれば右辺を代入
let a = null;
let b = 10;
a ||= b; // a が falsy なら b を代入
console.log(a); // 10
let c = false;
c &&= b; // c が truthy なら b を代入
console.log(c); // false(c は falsy なので代入されない)
これらの演算子は、条件に基づいて値を代入する処理を短く書くのに便利です。
Private Methods and Fields
- 導入
- ECMAScript 2022(ES13)
- TypeScript サポート
- TypeScript 3.8(2020年2月)
クラス内で プライベートメソッドやフィールド を定義するための新しい構文が導入されました。#
プレフィックスを使ってプライベートメソッドやフィールドを定義します。これにより、クラスの外からアクセスできないメソッドやプロパティを作成することができます。
class MyClass {
#privateField: string;
constructor() {
this.#privateField = "Hello, World!";
}
#privateMethod() {
console.log(this.#privateField);
}
public callPrivateMethod() {
this.#privateMethod();
}
}
const obj = new MyClass();
obj.callPrivateMethod(); // "Hello, World!"
// obj.#privateMethod(); // エラー: プライベートメソッドにはアクセスできない
#privateField
や #privateMethod
はクラスの外部からアクセスできません。
Top-Level await
- 導入
- ECMAScript 2022(ES13)
- TypeScript サポート
- TypeScript 4.4(2021年8月)
これにより、モジュール内で直接 await
を使って非同期操作を行うことができるようになりました。従来、await
は関数内でのみ使えましたが、トップレベルで直接使えるようになり、非同期コードが簡潔に書けるようになりました。
// モジュールスコープで直接 await を使う
const data = await fetchDataFromApi();
console.log(data);
従来、非同期操作をトップレベルで行うには、async
関数を使う必要がありましたが、これでその制約が解消されました。
globalThis
- 導入
- ECMAScript 2020(ES11)
- TypeScript サポート
- TypeScript 3.4(2019年3月)
globalThis
は、グローバルオブジェクトを指し示すための新しい方法です。window(ブラウザ)、global(Node.js)などの環境依存のグローバルオブジェクトへアクセスする代わりに、globalThis
を使用することで、どの環境でも一貫してアクセスできます。
console.log(globalThis); // グローバルオブジェクトを参照
globalThis
は、ブラウザ、Node.js、Web Workers など、どの実行環境でも同じようにグローバルオブジェクトを参照できます。