created_at
updated_at
tags
[tc39,javascript]

Nullish Coalescing と Optional Chaining

Intro

JS における null/undefined の扱い改善するための 2 つの機能が提案されている。

いずれも Stage 3 に進み、実装も始まっているので、現時点での解説を行う。

Nullish Coalescing

対象が null/undefined だった場合にデフォルト値を返したいといった場合を考える。

function main(option) {
  option.param = option.param || 'default'
}

main({param : 'hello'})

しかし、この場合は null/undefined 以外にも param が 0, false, '' など falsy な値の場合も上書きされてしまう。

var param;

param = 100
param = param || 'default' // 100

param = null
param = param || 'default' // 'default'

param = undefined
param = param || 'default' // 'default'

param = 0
param = param || 'default' // 'default'

param = false
param = param || 'default' // 'default'

param = ''
param = param || 'default' // 'default'

つまり、本来は null/undefined の厳密な判定を行わなくてはいけないわけだが、プロパティごとにそれを行うのは面倒だ。

そこで提案されたのが ?? という Null Coalescing Operator だ。(日本語だと Null 結合演算子になるのだろうか?)

これを使うと、 null/undefined であった場合のみ上書きができる。(名前に反して null だけではない点に注意)

var param;

param = 100
param = param ?? 'default' // 100

param = null
param = param ?? 'default' // 'default'

param = undefined
param = param ?? 'default' // 'default'

param = 0
param = param ?? 'default' // 0

param = false
param = param ?? 'default' // false

param = ''
param = param ?? 'default' // ''

パラメータの初期化などで、 null/undefined 以外の falsy な値を尊重しつつデフォルトを決めるといったケースに使うことができる。

function main(option) {
  option.message  = option.message ?? 'default message'
  option.timeout  = option.timeout ?? 100
  option.flag     = option.flag    ?? true
  option.logging  = {debug: true}

  cosnole.log(option)
}

main({
 message: '',
 timeout: 0,
 flag:    false,
})

Optional Chaining

null に対するプロパティアクセスはエラーとなるため、それを防ぐためには一度チェックを行う必要がある。

if (a !== null) {
  a.b()
}

アクセスできない場合 undefined を返すように三項演算子で書くとこうなる。

const result = (a !== null) ? a.b() : undefined

このショートハンドとしてアクセサの直前に ? を書くことができるようなるのがこの提案だ。

a?.b   // a == null ? undefined : a.b
a?.b() // a == null ? undefined : a.b()
a?.[x] // a == null ? undefined : a[x]

プロパティ以外にも、単体の関数を呼ぶ場合も利用可能だ。

a?.() // a == null ? undefined : a()

Short Circuiting

以下のように Chain しても、途中で undefined に対する呼び出しになってエラーになったりはしない。

a = null
a?.b?.c() // a? で undefined になるが、後続の .b? がエラーになるわけではない

これは ?. の左側(left-hand side)が null/undefined と評価された時点で全体の評価が決定し、 ?. の右側(right-hand side) は評価されていないからだ。

したがって、以下のように副作用のある処理も実行されない。

a = null
a?.b[x++] // ?. より右は実行されず x は増えない

この挙動を Short Circuting と言う。

もし Short Circuit を止めて、評価を実行したい場合は、対象を括弧でくくればその範囲のみに限定することもできる。

(a?.b).c // a が null でも .c は実行される

nullable と optional

こんなコードを考えてみる。

<body>
  <code class='highlight language-js'>
    console.log('hello')
  </code>
</body>
<script>
const lang = document.querySelector('code') // <code>
                     .classList             // [highlight, language-js]
                     .item(1)               // language-js
                     .split('-')[1]         // [language, js][1]
                     .toUpperCase()         // JS

console.log(lang) // JS
</script>

このコードをそのままブラウザで走らせれば、 JS が期待している DOM が確実にあるため問題なく動くだろう。

しかし、型を厳密に考えると querySelector()item() は nullable であり、配列の範囲外の添字アクセスは undefined を返す。

したがって、チェインのどこかが null/undefined になれば破綻するため、そこをケアするには各段階で確認する必要がある。

色々書き方はあるが、雑に書くとこういうことだ。

<body>
  <code class='highlight language-js'>
    console.log('hello')
  </code>
</body>
<script>
const $code = document.querySelector('code') // <code>
if ($code) {
  const secondClass = $code.classList.item(1) // language-js
  if (secondClass) {
    const lowerLanguage = secondClass.split('-')[1] // js
    if (lowerLanguage) {
      const lang = lowerLanguage.toUpperCase() // JS
      console.log(lang)
    }
  }
}
</script>

なお、本来は全ての if に対し、前提が崩れていた際の else が必要であるが、ここでは省略している。

大抵の DOM 操作はこの else 部分はもとより、 if 部分すら書かず、暗黙の前提を置いている場合も多い。

TypeScript を使うと、この if を通して null を剥がさないとエラーになるため、これを書く場合も増えてきた。

しかし else は、書いたところでできることが少なく、共通のエラーを表示する程度な場合も多いだろう。

Optional Chaining を使うと以下のようになる。

<body>
  <code class='highlight language-js'>
    console.log('hello')
  </code>
</body>
<script>
const lang = document.querySelector('code')? // node が無ければ null
                     .classList.item(1)?     // 2 つめの class が無ければ null
                     .split('-')[1]?         // 2 つめの 分割結果が無ければ undefined
                     .toUpperCase()

if (lang === undefined) {
  cosnole.error('something wrong')
} else {
  console.log(lang)
}
</script>

最後の lang は全ての前提が揃った場合に "JS" が入り、どこかが破綻すると undefined になる。

ここでの undefined の処理は、前述の if 版で省略されている else の処理にあたる。

つまり、 Optional Chaining は、こうした null を考慮すべき処理の連続を、全て if-else で分岐しつつケアする代わりに、一連の処理をつなげ結果を期待した値 or undefined に丸め込んで結果を検証するといった書き方を可能とする。

もしこのケースで、存在しない場合の lang をデフォルトで "TEXT" にしたい場合は、前述の Nullish Coalescing と組み合わせると以下のように書ける。

const lang = document.querySelector('code')? // node が無ければ null
                     .classList.item(1)?     // 2 つめの class が無ければ null
                     .split('-')[1]?         // 2 つめの 分割結果が無ければ undefined
                     .toUpperCase()          // どこかで失敗していれば undefined
                     ?? 'TEXT'               // undefined だった場合のデフォルト値

console.log(lang) // "JS" or "TEXT"

どこまでを一連の Optional でくくるか、途中の処理の結果の型は何か、といった設計に注意すればかなり便利に使えるだろう。

他の言語で、何かしら似たような、そしておそらくもっと高機能な機能と比べれば至らない部分も多だろうが、無いことろ比べればだいぶマシになりそうだ。

実装

どちらもトランスパイルしやすい仕様なので、各ツールでは以前から実装されており、探せば色々出てくるだろう。

ブラウザについては以下だ。

Nullish Coalescing

Safari TP89 に入っているが、有効にするには Runtime Flag が必要だった。

Mac の場合 JSC のフラグは以下のように付与する。

$ __XPC_JSC_useNullishCoalescing=true open -a 'Safari Technology Preview'

Optional Chaining

Intents は出ているが現時点の Chrome Canary では動かなかった。

Safari もパッチはあるが、 TP にも入っていない。

DEMO

動作するデモを以下に用意した。