メインコンテンツまでスキップ

ヌリッシュ合体

· 約9分
ジャスティン・リッジウェル

ヌリッシュ合体提案 nullish coalescing proposal (??) は、デフォルト値を処理するために設計された新しいショートサーキット演算子を追加します。

既に他のショートサーキット演算子 &&|| に慣れているかもしれません。これらの両演算子は「truthy」と「falsy」の値を扱います。コード例 lhs && rhs を想像してください。もし lhs (左辺) が falsy なら、式は lhs を評価します。それ以外の場合、式は rhs (右辺) を評価します。コード例 lhs || rhs の場合、その逆が真となります。もし lhs が truthy なら、式は lhs を評価します。それ以外の場合、式は rhs を評価します。

では、「truthy」と「falsy」とは具体的に何を意味するのでしょうか? スペックの用語では、これらは ToBoolean 抽象操作に相当します。通常のJavaScript開発者にとっては、すべての値がtruthyですが、一部のfalsy値にはundefinednullfalse0NaN、空文字列''が含まれます。(技術的にはdocument.allに関連する値もfalsyですが、それについては後ほど触れます。)

では、&&および||の問題点は何でしょうか?そして、なぜ新しいヌリッシュ合体演算子が必要なのでしょうか?それはtruthyとfalsyの定義がすべてのシナリオに適合せず、これがバグにつながるためです。以下を想像してください。

function Component(props) {
const enable = props.enabled || true;
// …
}

この例では、enabledプロパティをコンポーネント内の機能が有効化されているかどうかを制御するオプションのブールプロパティとして扱います。つまり、enabledを明示的にtrueまたはfalseに設定できます。しかし、これは_オプション_プロパティであるため、設定を省略することで暗黙的にundefinedに設定できます。undefinedの場合、コンポーネントをenabled = true(デフォルト値)として扱いたいと考えています。

この時点で、コード例にバグがあることに気づくことができると思います。enabled = trueを明示的に設定すると、enable変数はtrueになります。設定を省略して暗黙的にenabled = undefinedとすると、enable変数はtrueになります。しかし、enabled = falseを明示的に設定しても、enable変数は引き続きtrueになります!本来の意図は値をデフォルトでtrueにすることでしたが、実際には値を強制してしまいました。この場合の修正は期待する値について非常に明示的になることです。

function Component(props) {
const enable = props.enabled !== false;
// …
}

この種のバグはすべてのfalsy値で頻繁に発生します。これが非常に簡単にオプションの文字列(空文字列''が有効な入力と見なされる場合)やオプションの数値(0が有効な入力と見なされる場合)に関連する場合があるのです。この問題があまりにも一般的であるため、このようなデフォルト値割り当てを処理するためにヌリッシュ合体演算子を導入しています。

function Component(props) {
const enable = props.enabled ?? true;
// …
}

ヌリッシュ合体演算子(??)は||演算子に非常に似ていますが、演算子を評価する際に「truthy」を使用しません。その代わりに「nullish」の定義、つまり「値がnullまたはundefinedと厳密に等しいか」を使用します。式lhs ?? rhsを想像してください。もしlhsがnullishでないなら、式はlhsを評価します。それ以外の場合、式はrhsを評価します。

具体的には、false0NaN、空文字列''はすべてfalsy値であるがnullishではありません。そのようなfalsyであるがnullishでない値がlhs ?? rhsの左辺である場合、式は右辺ではなくそれらを評価します。バグを撃退!

false ?? true;   // => false
0 ?? 1; // => 0
'' ?? 'default'; // => ''

null ?? []; // => []
undefined ?? []; // => []

分解代入の際のデフォルト割り当てについては?

最後のコード例は、オブジェクトで分解代入を使用してデフォルト割り当てを行うことで解決できることに気付いたかもしれません。

function Component(props) {
const {
enabled: enable = true,
} = props;
// …
}

やや説明的な記述ですが、完全に有効なJavaScriptです。ただし、多少異なるセマンティクスを使用します。オブジェクト分解内のデフォルト割り当ては、プロパティがundefinedと厳密に等しい場合に割り当てをデフォルトにします。

しかし、undefinedのみを対象にした厳密な等価テストは常に望ましいわけではなく、分解対象のオブジェクトが必ずしも利用可能であるわけではありません。例えば、関数の返り値(分解するオブジェクトがない場合)のデフォルト化、または関数がnull(DOM APIでは一般的)を返す場合などです。このような時にヌリッシュ合体を使用すべきなのです。

// 簡潔なヌリッシュ合体
const link = document.querySelector('link') ?? document.createElement('link');

// デフォルトのアサインメント構文でボイラープレート
const {
link = document.createElement('link'),
} = {
link: document.querySelector('link') || undefined
};

さらに、optional chainingのような新しい機能の一部は、分割代入とうまく動作しない場合があります。分割代入ではオブジェクトが必要なため、optional chainingがundefinedを返したときに例外処理をする必要があります。一方で、nullish coalescingではそのような問題はありません。

// Optional chainingとnullish coalescingの組み合わせ
const link = obj.deep?.container.link ?? document.createElement('link');

// Optional chainingを使用したデフォルトの分割代入
const {
link = document.createElement('link'),
} = (obj.deep?.container || {});

演算子のミックスとマッチ

言語設計は難しいもので、開発者の意図に曖昧さが生じることなく新しい演算子を作るのは簡単ではありません。例えば、&&||演算子を混在させたことがあるなら、この曖昧さに直面したことがあるでしょう。例えば、式lhs && middle || rhsを考えてみましょう。JavaScriptでは、これは実際には(lhs && middle) || rhsとして解析されます。一方、式lhs || middle && rhsの場合、実際にはlhs || (middle && rhs)として解析されます。

&&演算子は||演算子よりも左辺および右辺に対して高い優先順位を持つため、暗黙の括弧は&&を囲むように解釈されます。??演算子を設計する際には、その優先順位をどのようにするかを決める必要がありました。それには次のような可能性がありました。

  1. &&||よりも低い優先順位
  2. &&よりも低く、||よりも高い優先順位
  3. &&||よりも高い優先順位

これらの優先順位定義のそれぞれに対して、4つのテストケースを検討する必要がありました。

  1. lhs && middle ?? rhs
  2. lhs ?? middle && rhs
  3. lhs || middle ?? rhs
  4. lhs ?? middle || rhs

各テスト式において、暗黙的な括弧がどこに入るべきかを決定しなければなりませんでした。そして、それが開発者の意図した通りに式を正確に包まなければ、不適切なコードを書くことになる可能性があります。残念ながら、どの優先順位レベルを選んでもあるテスト式が開発者の意図を侵害する可能性がありました。

最終的に、??と(&&または||)を混在させる際は明示的な括弧が必要とされる設計にしました。(括弧表示のグループについても明確にしました!ミタジョーク!)演算子グループの1つを括弧で囲まないと構文エラーが発生します。

// 明示的な括弧グループが混在時に必要
(lhs && middle) ?? rhs;
lhs && (middle ?? rhs);

(lhs ?? middle) && rhs;
lhs ?? (middle && rhs);

(lhs || middle) ?? rhs;
lhs || (middle ?? rhs);

(lhs ?? middle) || rhs;
lhs ?? (middle || rhs);

このようにすれば、言語のパーサーは開発者の意図に常に一致します。また、後からコードを読む人にもすぐに理解できます。素晴らしいですね!

document.allについて教えて

document.allは、絶対に使うべきではない特殊な値です。ただし、使用する場合は、それが「truthy」および「nullish」とどのように相互作用するかを知っておくと役立ちます。

document.allは配列のようなオブジェクトであり、配列のようにインデックス付きプロパティやlengthを持っています。オブジェクトは通常truthyですが、驚いたことにdocument.allはfalsyな値のように振る舞います!実際には、nullおよびundefinedの両方と緩やかに等しいのです(通常これによってプロパティを持つことができなくなります)。

&&または||と一緒に使用すると、document.allはfalsyのように振る舞います。しかし、厳密にはnullundefinedと等しくないため、nullishではありません。このため、document.all??とともに使用すると、他のオブジェクトと同じように動作します。

document.all || true; // => true
document.all ?? true; // => HTMLAllCollection[]

nullish coalescingのサポート