みなさん、Optional Chaining使ってますか? 私は先日出たTypeScript 3.7 Betaを小さいプロジェクトに導入して使ってみました。これはとても快適ですね。
例によって、Optional ChainingはECMAScriptに対するプロポーザルの一つです。つまり、もうすぐ入りそうなJavaScriptの新機能です。プロポーザルはたくさんの種類がありますが、その中でもOptional Chainingはその高い有用性からこれまで多くの注目を集めてきました。Optional Chainingは2019年6月のTC39ミーティングでStage 3に上昇し、いよいよ正式採用が近く期待も高まってきたところです。TypeScript 3.7にも導入されたため、TypeScriptユーザーの方々は11月上旬に正式リリースが予定されているTypeScript 3.7を今か今かと待ち構えていることでしょう(筆者のようにフライングしてBetaで使い始めている人もいるかもしれません)。また、Babelのプラグインも前々から存在したため熱心な方々はもう使っているかもしれません。
となると、巷に、あるいはこのQiitaにもOptional Chainingに関する記事がいくつもあります。そこで自分もOptional Chainingの解説がしたくなってできたのがこの記事です。「もうQiitaにOptional Chainingの記事あるのに今さら?」と思われるかもしれませんが、
この記事を読めばOptional Chainingの全てがわかります。(強気)
期待してお読みください。なお、Optional Chainingに関する一次的な情報源は(ECMAScriptに正式採用されるまでは)以下のプロポーザルです。一次情報にあたるのが好きな方はぜひ目を通してみましょう。
Optional Chainingの基本
Optional Chainingとは、?.
という新しい構文です。
もっとも基本的な用法はプロパティアクセスの.
の代わりに?.
を使うものです。obj?.foo
とした場合、obj
がnullishな値(null
またはundefined
)の場合は結果がundefined
となり、そうでない場合はobj.foo
が結果となります。
つまり、obj?.foo
というのはおおよそobj != null ? obj.foo : undefined
と同じです1。
たとえobj
がnull
だったとしてもobj?.foo
はnull
ではなくundefined
になります。間違えやすいので注意しましょう2。
背景
この機能の背景には、undefined
またはnull
に対してプロパティアクセスを行うとエラーになるという言語仕様があります。すなわち、obj
がnull
とかの時にobj.foo
を実行するとエラーとなります。
ちなみに、プロパティアクセスがエラーとなるのはこの場合のみで、それ以外の場合(foo
というプロパティを持たないオブジェクトや、あるいは数値や文字列といった他のプリミティブ)はエラーにはなりません3。foo
というプロパティがない場合、エラーにはならずobj.foo
がundefined
となります。
このことから、nullishかもしれない値obj
に対してはobj.foo
のようなプロパティアクセスをいきなり行うのは危険であり、前述のようにobj != null
というようなチェックが不可欠でした。逆に言えば、nullishな値さえ排除すればとりあえずプロパティの値を取得するところまでは漕ぎ着けることができます(取得できた値がundefined
かもしれませんが)。
ということで、nullishな値に対するチェックをより簡単に行えるようにするのが?.
構文ということになります。obj?.foo
ならば、obj
がnullishな値であってもエラーが発生せず、代わりにundefined
が結果となります。これにより、残りの場合と一緒に扱うことができるようになるケースは結構多いと思われます。特に、Nullish Coalescing(??
演算子)と組み合わせることで、明示的に条件分岐を書かずにnullishな値を適切に処理することができるケースが増えるでしょう。これはとてもうれしい点です。
プロポーザルによれば、?.
に類似の構文は以前からGroovy, C#, Swift, CoffeeScriptなどに存在していました。この中ではGroovyが最も古く、バージョン1からすでに存在していたようです。その次が恐らくCoffeeScriptで、これはAltJSであるということもあり今回のプロポーザルに直接的な影響を与えています。
3種類の構文
厳密には、今回のプロポーザルで?.
に関連する構文が3種類追加されます。
obj?.foo
obj?.[expr]
obj?.(arg1, arg2)
一番上はこれまで説明していたやつです。二番目は、お分かりの通りobj[expr]
用の構文で、これをobj?.[expr]
に変えることでobj
がnullishの場合はエラーではなくundefined
が返るようになります。
三番目はオプショナル関数呼び出し構文です。この例は引数2つですがもちろん何個でも構いません。obj(arg1, arg2)
はobj
がnullishのときはやはりエラーとなりますが、obj?.(arg1, arg2)
はobj
がnullish
のときは何も行わずにundefined
となります。なお、obj
がnullishではないが関数でもない場合は依然としてエラーになりますので注意しましょう。
3種類が一貫して?.
というトークンを持っているので分かりやすくていいですね。ただ、?.
の直後に[ ]
とか( )
のような括弧が来る構文が気持ち悪いという人もいるようです(詳しくはすこし後で解説します)。
ちなみに、代入には対応していません。つまり、obj?.foo = 123
(obj
が存在するときのみfoo
プロパティに123
を代入する)のようなことはできません。
また、ES2015ではobj.foo`123`
のようにタグ付きテンプレートリテラルの記法で関数を呼び出すことが可能になりましたが、これはOptional Chainingのサポート外です。つまり、obj?.foo`123`
のようなことはできません。func?.`123`
もだめです。
短絡評価
Optional Chainingを語る上で外せないのが短絡評価です。実はここがOptional Chainingを理解する上で肝となる部分です。将来足元を掬われないように今のうちにしっかりと理解しておきましょう。
短絡評価とは
短絡評価という単語は、&&
や||
に絡んで聞いたことがある方が多いでしょう。これらの演算子は、左側を評価した時点で結果が確定したのであれば右側が評価されません。これを短絡評価と呼びます。
const foo = 123;
// fooが真なので func() は呼び出されない
const v1 = foo || func();
上の例では、||
の結果は左側のfoo
を評価した時点で確定します。よって、右側は意味がないのでそもそも評価されないことになります。
?.
の短絡評価
?.
の短絡評価についても基本は変わりません。左側を評価した時点で結果が確定するならば、右側は評価されないというのが原則です。
?.
の場合は、左側がnullishな値だと判明した時点で結果がundefined
に確定します。よって、その場合は右側は評価されません。このことは例えば次のような場合に影響があります。
const foo = obj?.[getKey()];
この例では、obj
がnullishならばfoo
にはundefined
が入り、そうでなければfoo
にはobj[getKey()]
が入ります。キーの名前([ ]
の中身)を得るにはgetKey()
という関数呼び出しを評価する必要があります。
ポイントは、短絡評価により**obj
がnullishな値ならばgetKey()
は呼び出されない**という点です。まあ、これは次と同じと考えれば自然な動作ですね。この場合も、getKey()
が呼び出されるのはobj
がnullishでないときだけです。
const foo = obj != null ? obj[getKey()] : undefined;
関数呼び出しの構文でも同じです。次の例では、func
がnullishな値のときはfoo()
, bar()
, baz()
は計算されません。
func?.(foo(), bar(), baz())
オプショナルチェーンの短絡評価
短絡評価の応用例として、次のような場合を考えてみてください。
obj?.foo.bar.baz
次の3種類の場合にこれの評価結果はどうなるでしょうか。
-
obj
が{ foo: { bar: { baz: 123 } } }
の場合。 -
obj
が{ foo: { bar: null } }
の場合。 -
obj
がundefined
の場合。
正解はこうです。
-
123
になる。 -
obj?.foo.bar
がnull
になり、null
のbaz
プロパティを読もうとしてエラーが発生する。 -
undefined
になる。
特に3がポイントです。obj
がnullishな値だったことにより、そのあとの部分全部(?.foo.bar.baz
)が無視されます。
これに関しては、次のような誤解が発生しやすいので注意が必要です。
-
123
になる。 - エラーになる。
- エラーになる。(誤解)
3の誤解は次のような考え方をすると発生しがちです。
-
obj?.foo.bar.baz
のobj?.foo
部分がまず評価されてundefined
になる。 - すると
undefined.bar.baz
が評価されることになる。 -
undefined
のbar
プロパティを読もうとしてエラーになる。
繰り返しますが、この誤解を避けるために抑えるべきポイントはobj?.foo.bar.baz
でobj
がnullishな値の場合は?.foo.bar.baz
全体が飛ばされるということです。
ちなみに、このように?.
から始まるプロパティアクセス(または関数呼び出し)の列のことをオプショナルチェーン (optional chain)と呼びます。?.foo.bar.baz
は一つのオプショナルチェーンです。この用語を使うと、?.
の評価は左側がnullishな値の場合はオプショナルチェーン全体を無視してundefined
を返すものだと言うことができます。
オプショナルチェーンは[ ]
によるプロパティアクセスや()
による関数呼び出しを含むことができるので、次のようなものも一つのオプショナルチェーンです。
?.foo.bar["hoge fuga"](1, 2, 3).baz
オプショナルチェーンと括弧
上の例を少し変えてこうしてみましょう。
(obj?.foo.bar).baz
この場合、括弧で区切られているので?.foo.bar
までがオプショナルチェーンであり.baz
はチェーンに含まれません。よって、obj
がnullishな値だった場合はまずobj?.foo.bar
がundefined
に評価され、undefined.baz
を計算しようとしてエラーになります。
このように、括弧によってオプショナルチェーンが区切られてプログラムの意味が変わることがある点は要注意です。これはオプショナルチェーンの新しい特徴です。従来はobj.foo.bar.baz
を(obj.foo.bar).baz
に変えても意味は変わりませんでした。
オプショナルチェーンに間違えて括弧をはさむ人はあまり居ないと思いますが、3ヶ月に1回くらいこんな罠を踏むかもしれませんから気をつけましょう。
複数のオプショナルチェーン
ところで、次のような式を考えることもできます。
obj?.foo.bar?.baz(123)
?.
が2箇所に出てきました。この式はどのように解釈できるでしょうか。
実は、これはobj
のあとに2つのオプショナルチェーンが続いている形になっています。1つ目は?.foo.bar
で、2つ目が?.baz(123)
です。
先ほどオプショナルチェーンという概念を紹介しましたが、これは先頭のみに?.
が来るものであり、次の?.
が来た時点でそこから先は次のオプショナルチェーンになります。上の式は以下のように括弧を付けても同じことです。
(obj?.foo.bar)?.baz(123)
途中でundefined
やnull
が発生する可能性があるときは複数のオプショナルチェーンを繋げる意味もあります。例えばobj
が{ foo: { bar: null } }
だった場合を考えましょう。このとき、上の式の結果はundefined
となります。まずobj?.foo.bar
がnull
になり、それに対して次のオプショナルチェーン?.baz(123)
が適用されますが、?.
の左はnull
なので全体の結果はundefined
となります。
一方で、?.
がひとつだけの場合は違った結果になります。上のobj
に対してobj?.foo.bar.baz(123)
を評価した場合を考えると、これはobj?.foo.bar
まで評価してnull
を得たところでそれのbaz
プロパティを取得しようとしてエラーになります。
?.[ ]
と?.( )
?.
という構文は一貫しているとはいえ、特にobj?.[expr]
とかfunc?.()
という構文は.
の後に括弧が来る点が気持ち悪いと思えるかもしれません。どちらかというとobj?[expr]
とかfunc?()
のほうがきれいです。
しかし、もちろんそうはできない理由がありました。それは条件演算子 cond ? expr1 : expr2
の存在です。どちらも?
を使っていることがあり、両方の構文があるとたいへん紛らわしくなります~~(:
があるので多分文法が曖昧になるというわけではなさそうですが)~~ 4。
プロポーザル文書に載っている例を引用します。
obj?[expr].filter(fun):0
4文字目の?
は条件演算子の?
なのですが、:
を見るまではobj?[expr].filter(fun)
というオプショナルチェーンである可能性を捨て切れません。このように判断を遅延しなければいけないのは人間にも処理系にも負担になります。
これを避けるために?.
というトークンを使っているのです。
?.
と数値リテラル
実は、?.
を用いてもなお文法上の問題が多少残っています。それは、.1
のような数値リテラルが関わる場合です。実は数値リテラルは.1
のようにいきなり少数点から始めることができます。これは0.1
と同じ意味です。これまたプロポーザルから引用しますが、次のような場合が問題になります。
foo?.3:0
これの正しい解釈はfoo ? .3 : 0
、つまり条件演算子です。コード中に?.
の並びがありますが、これはoptional chainingの構文ではなく?
と.3
が並んだものです。
このことを表現するために、「Optional Chainingの?.
構文の直後に数字(0
〜9
)が来ることはない」という規則が用意されています。これにより、?.3
という並びを見た時点で即座にこれは?. 3
ではなく? .3
であることが判断できるようになっています。
そもそもobj.3
のようなプロパティアクセスは文法上不可能ですからobj?.3
と書いてあってもobj ?. 3
と解釈する必要はないように思えます。それにも関わらず上記のような規則があるのは、極力分岐を減らしてパーサーの負担を減らすためでしょう。
そもそも、JavaScriptの文法というのは原則として前から順番に読んでいけば常に解釈が1通りに定まり、複数の可能性が同時に存在しないように定義されています(これが達成できていないところもあるのですが、そういうところは仕様上ではカバー文法を用いて明示的な配慮がされています)。
今回も、パーサーがプログラムを前から読んで?.
まで読んだ瞬間に、これが?.
というトークンなのかそれとも?
が条件演算子でその後に何か別の.
が続いているのかを決定できるのが理想です。ただ、foo?.3:0
とfoo?.bar
というプログラムが両方存在する可能性がある以上、これだけの情報からではこれは不可能です。
しかし、実は1文字先読みをすることでこれが可能になります。つまり、.
の次の文字が数値ならばその時点で?.
の?
が条件演算子であることが確定し、そうでなければ?.
はOptional Chainingの開始であることが確定します。
一般に長く先読みするほどパースが大変になりますが、まあ1文字先読みくらいなら許容範囲です。
以上がOptional Chainingの基本でした。おおよそプロポーザルの文書に書いてあることを説明した感じです。この文書も分かりやすく書かれていますので一度目を通してみてもよいかもしれません。
他の言語との比較
CoffeeScriptを始めとして、Optional Chainingに類似の言語機能をすでに持っているプログラミング言語はいくつか存在します。ここでは、他の言語とJavaScriptのOptional Chainingとの違いや共通点を明らかにします。
以下の言語はプロポーザルの文書に言及があったものを列挙しています。他にこんな言語にもあるよという情報は大歓迎です。
Groovy
一番手は確認されている中で最も古くから?.
を持つGroovyです。GroovyではこれはSafe Navigation Operatorと呼ばれています。
シンプルにobj?.foo
という形がサポートされています。?.
の挙動は言語によって多少異なり、上記で説明した短絡評価があるかないかの2種類に大別されるのですが、Groovyはどちらなのかよく分かりませんでした。調べようとも思いましたがプロポーザル文書にも「よく分からなかったわ」と書いてあるので多分大変なのだろうと思い調べていません。これに関する情報をお持ちの方はぜひお寄せください。
CoffeeScript
次はCoffeeScriptです。CoffeeScriptではこの機能はExistential Operatorと呼ばれ、JavaScriptと同じく3種類の構文を持ちます。それぞれの構文は以下のように表現されます。
obj?.foo
obj?[expr]
-
func?(arg)
(または関数呼び出しの括弧を省略してfunc? arg
)
まずは構文を比較します。CoffeeScriptでも?.
という演算子を用いてobj?.foo
のように書くことができます。ただ、[ ]
と( )
に関してはJavaScriptとは異なり、obj?[expr]
やfunc?(arg)
のように書くことができました。
JavaScriptとは異なりこれらの場合に?.[expr]
としなくても良かった理由は、CoffeeScriptが条件演算子?:
を採用していないからです。代わりにif
が式となっており、条件演算子のように使用できます。
挙動については、現在のJavaScriptのものと基本的に同じです。つまり、obj?.a.b.c
でobj
がundefined
の場合はチェーン全体が飛ばされるという挙動をします。短絡評価についても同じであるほか、括弧でチェーンを切ることができる点も同じです。
ただ、CoffeeScriptでは今のJavaScriptにはないいくつかの追加機能を備えていました。ひとつはオプショナルな代入です。
obj?.foo = 123
このようなプログラムが可能であり、これはif (obj != null) obj.foo = 123;
とおおよそ同じ意味でした。
また、タグ付きテンプレートリテラルの関数部分でも?.
が使用可能です。
obj?.foo"""123"""
JavaScriptでは、前者は仕様が複雑になることから、後者はユースケースの欠如から採用されていない仕様です。CoffeeScriptの大胆さと言語デザインが伝わってくる例ですね。
長くなりましたが、他の言語はあまり詳しいわけではないのでさらっと流します。
C#
C#ではこれはNull-conditional operator(Null条件演算子)と呼ばれています。2015年リリースのC# 6で追加されたようです。
C#ではobj?.foo
とobj?[expr]
の2種類の構文がサポートされています。C#にも条件演算子? :
があるはずですが、JavaScriptとは異なりobj?[expr]
の形が採用されています。これは、C#では[expr]
のような式が存在しないことが理由と推測されます。?
の後が[
である時点で?
が条件演算子であることが確定できるのです。JavaScriptと同様に1文字先読みで済んでいます。
短絡評価周りの挙動も、基本的に今回解説したJavaScriptのものと同様です。
なお、C#は(というかほとんどの言語は)undefined
とnull
が別々にあるみたいな意味不明な状況にはありません。C#ではnull
があり、?.
の左辺がnull
のときに結果がnull
になるという仕様です。
オプショナル関数呼び出しfunc?.()
にあたる構文はありませんが、C#ではデリゲート(thisを覚えている関数オブジェクトのようなものらしいです)がInvoke
を持っており、func?.Invoke()
のようにして代替可能です。JavaScriptでも関数オブジェクトがcall
メソッドなどを持ってはいますが、thisを渡さないといけないせいで微妙に使い勝手が悪くなっています。
obj?.foo = 123
はサポートされていません。
Swift
Swiftではこの機能はOptional Chainingです。同じ名前ですね。Swiftではnil
がこの機能の対象です。
Swiftでも構文はやはりobj?.foo
とobj?[expr]
です。オプショナル関数呼び出しは無いようです。
挙動はJavaScriptと同じく、チェーンの短絡評価および括弧でチェーンを切る挙動ができます。
また、代入におけるOptional Chaining(obj?.foo = 123
)もサポートしています。面白い点はこの代入式の返り値がVoid?
型であり、代入が成功したか失敗したかが返り値から分かるようになっている点ですね。
Kotlin
Kotlinも?.
演算子を持ち、これはSafe Callと呼ばれているようです。
Kotlinでは、これまでの言語とは異なりオプショナルチェーンの概念は存在しません。obj?.foo.bar
は(obj?.foo).bar
と同じであり、obj
がnull
の場合は.foo
は飛ばされますが.bar
は飛ばされません。これはobj?.foo?.bar
と書く必要があります(Kotlinはいわゆるnull
安全な型システムを持っているので、こう書かないとコンパイルエラーとなります)。
なお、obj?[expr]
に相当する記法は無いようです。例えばList<Int>?
型からInt?
を取り出したい場合、list?.get(0)
5のようにするかlist?.let { it[0] }
とする必要があります(あまり自信がないので間違っていたらぜひ訂正をお願いします)。
obj?.foo = 123
は可能です。
Dart
Conditional Member Accessと呼ばれ、?.
演算子のみが存在するようです。代入も可能です。
また、Kotlinと同様に一段階のみのサポートです。
Ruby
RubyではSafe Navigation Operatorと呼ばれており、Ruby 2.3で導入された機能のようです。
Rubyではこれは&.
という名前です。これまでの言語で唯一?
を含んでいませんが、まあこれは仕方ありませんね。Rubyは識別子(メソッド名など)に?
や!
を含むことができる言語なので、演算子に?
を使うのはさすがに都合が悪そうです。
Kotlinなどと同様に&.
は一段階しか作用しません。Rubyはドキュメントにこのことが明記してあってたいへんありがたいですね。
余談:Elm
ちょっと趣向を変えて、というか趣味に走っていますが、関数型言語との比較もしてみます。そもそも関数型言語はオブジェクトとかプロパティという概念を持たないこともあるので比較の意味がそこまで大きいわけではありません。なので余談ということにしてみました。
さて、値が無いかもしれない(null
かもしれない)という状況に対して、これから説明するように関数型言語はかなり異なる方法で対処します。
関数型言語の場合しっかりとした代数的データ型を備えていることが多く、null
のような概念の代わりになるものとしてMaybe
型のようなデータ構造を持つのが典型的です。ElmのMaybe型はHaskellと同じ定義で、例えばMaybe Int
型はJust 42
のようなInt
型の値をラップした値とNothing
から成ります。
また、Elmの場合はJavaScriptのオブジェクトに比較的近い概念としてレコードというものがあります。レコードはこのように使用します。
import Html exposing (text)
-- Person 型を定義(実態はただのレコード型)
type alias Person =
{ name: String
, age: Int
}
-- Person型の変数pを定義(型宣言は省略可)
p: Person
p =
{ name = "John Smith"
, age = 100
}
main =
text (p.name) -- "John Smith" が表示される
Person
が存在するかもしれないししないかもしれないという状況はMaybe Person
型で表現します。
p1: Maybe Person
p1 = Just
{ name = "John Smith"
, age = 100
}
p2: Maybe Person
p2 = Nothing
main =
text (p1.name) -- これはコンパイルエラー
Maybe Person
はPerson
とは別の型でありレコードではないためp1.name
のようなアクセス方法はできません。
また、Elmは?.name
のようなことができる機能はありません。筆者はElmに詳しくありませんが、やるとしたら恐らくこうでしょう。
n1 = Maybe.map .name p1 -- Just "John Smith"
n2 = Maybe.map .name p2 -- Nothing
Maybe.map
は与えられた関数をJust
の中身に適用する関数です(Nothing
のときはそのままNothing
が返る)。.name
はこれでひとつの関数であり、与えられたレコードのname
フィールドを返します。
ポイントは、無いかもしれない値(Maybe Person
型の値)を操作するにあたって2つの一般的な関数(Maybe.map
と.name
)を用いて対処している点です。むやみに演算子を増やすよりも関数の組み合わせで対処する点に関数型言語らしさが現れています。真に何もない値であり関数・メソッド等のサポートを受けにくいnull
と比べると、Maybe
は代数的データ型を用いて表現される値でありMaybe.map
に代表されるような標準ライブラリレベルでのサポートが受けやすい点が大きく異なっています。
ちょっと話が横道に逸れましたが、以上が他の言語との比較でした。他の言語の情報をお持ちの方はお寄せいただけるとたいへん幸いです。
TypeScriptとOptional Chaining
さて、ではJavaScriptに話を戻しましょう。……と言いたいところですが、次はTypeScriptに話を移します。TypeScriptは言わずと知れたJavaScriptの型付きバージョンです。
TypeScriptは、プロポーザルがStage 3になったらその機能を導入するという方針があるようです。ということで、Optional ChainingがTypeScriptに導入されるのは11月リリースのTypeScript 3.7です。現在すでにベータ版が出ており、これでTypeScriptのOptional Chainingサポートを体験できます。
ここではTypeScriptにおけるOptional Chainingの挙動を解説します。もはやTypeScriptがJavaScript開発における必須ツールとなりつつある今日この頃ですから、JavaScriptの新機能とあればTypeScriptにどう影響するのか気になるのは必然です。ということで、この記事では欲張りなことにTypeScriptにも手を伸ばして解説します。
とはいえTypeScriptなんか興味ありませんよという硬派(安全性的にはむしろ軟派?)な方もいるでしょうから、そのような方は次のセクションまで飛ばしましょう。また、TypeScriptの用語で分からないところがあればTypeScriptの型入門が参考になるかもしれません(宣伝)。
では、さっそく?.
の例をお見せします。
interface HasFoo {
foo: number;
}
const obj: HasFoo | undefined = Math.random() < 0.5 ? { foo: 123 } : undefined;
// これはエラー (objがundefinedかもしれないので)
const v1 = obj.foo;
// これはOK(v2はnumber | undefined型)
const v2 = obj?.foo;
HasFoo
型は、foo
というプロパティを持つオブジェクトの型です。今回は変数obj
をHasFoo | undefined
型として宣言しました。これは、obj
の中身はHasFoo
型のオブジェクトかもしれないしundefined
かもしれないということです。
このobj
に対してobj.foo
とすると、TypeScriptにより型エラーが発生します。これは、obj
がundefined
かもしれない状況でobj.foo
を実行するとエラーになるかもしれなくて危険だからです。TypeScriptは型エラーにより、そのような危険なコードを事前に警告してくれます。
一方、obj?.foo
は型エラーになりません。これは、?.
ならばたとえobj
がundefined
でもエラーが発生することはなく安全だからです。
その代わり、obj?.foo
の結果はnumber | undefined
型となります。これは、number
型かもしれないしundefined
型かもしれないという意味です。実際、obj
がundefined
のときはobj?.foo
の結果はundefined
になるし、obj
がundefined
でないときは(obj
がHasFoo
型になるので)obj?.foo
はnumber
になるためこの結果は妥当です。
型推論のしくみ
上では言葉でそれっぽい説明をしましたが、型推論の挙動を整理するのはそれほど難しくありません。これに関してはTypeScriptの当該プルリクエストも参考になるでしょう。
例えば、expr?.foo
という式の型を推論するにあたってはおよそ以下のような過程を減ることになります。
- 普通に
expr
の型を推論する(T
とする)。 -
T
がnull
やundefined
を含むunion型の場合:-
T
からnull
とundefined
を除いた型T2
を作る。 -
T2
のfoo
プロパティの型U
を得る。(foo
プロパティが無ければ型エラー) -
expr?.foo
の型をU | undefined
とする。
-
-
T
がnull
やundefined
を含むunion型ではない場合:- 普通に
expr.foo
の型を返す。(無いなら型エラー)
- 普通に
要するに、expr
から一旦null
やundefined
の可能性を除いて考えて、もしそういう可能性があるなら結果の型にundefined
をつけるということです。
never型に関する注意
知らないと少し混乱するかもしれない例がひとつありますのでここで紹介しておきます。それは、expr?.foo
でexpr
がただのundefined
型(あるいはnull
型とかundefined | null
型)だった場合です。
const obj = undefined;
// 型エラーが発生
// error TS2339: Property 'foo' does not exist on type 'never'.
const v = obj?.foo;
この例ではobj
はundefined
型です(もはやオブジェクトではないので変数の命名が微妙な気もしますが)。したがって、obj?.foo
は常に結果がundefined
となり、foo
プロパティへのアクセスが発生することはありません。
となるとobj?.foo
の型はundefined
型になりそうな気がしますが、実際はそうではありません。というか、実はこの式は型エラーとなります。
そもそも、obj
がundefined
であると判明しているのであればobj?.foo
は絶対にundefined
にあるのであり、わざわざこんな書き方をする意味はありません。何かを勘違いしている可能性が非常に高いでしょう。その意味では、これが型エラーになるのはどちらかといえば嬉しい挙動です。
問題なのはエラーメッセージです。エラーメッセージは「never
型の値にfoo
というプロパティはないのでobj?.foo
はだめですよ」と主張しています。ここで突如登場したnever
型の意味が分からないとエラーの意味がよく分からないのではないでしょうか。
never
型は「値が存在する可能性が無いことを表す型」です。obj?.foo
は「obj
がnull
やundefined
でないときはobj.foo
にアクセスする」という意味を持ちますが、ではobj
がundefined
のときにそこからnull
型やundefined
型の可能性を除外すると何が残るでしょうか。そう、何も残りませんね。この「何も可能性がない」状況を表してくれる概念がnever
型です。
要するに、「obj
がHasFoo | undefined
型のときは、foo
プロパティへのアクセスが発生するのはobj
がHasFoo
型のときである」のと同様に、「obj
がundefined
型のときは、foo
プロパティへのアクセスが発生するのはobj
がnever
型のときである」という理屈です。
そして結局のところ、never
型に対するプロパティアクセスは許可されません6。これが型エラーの原因です。ここで言いたいことは、エラーメッセージにnever
型が出てきたら「絶対に走らない処理」を書いていることを疑うべきだということです。今回の場合はobj?.foo
と書いても絶対にfoo
プロパティへのアクセスは発生しないのでした。
Optional Chainingと型の絞り込み
TypeScriptのたいへん便利な機能のひとつは、条件分岐の構造を理解し自動的に型の絞り込みを行なってくれることです(type narrowing)。実は、Optional Chainingも型の絞り込みに対応しています。
※ この内容は記事執筆時点でまだTypeScript 3.7 betaに入っていませんが、このプルリクエストで実装されているためTypeScript 3.7に導入されることが期待されます。また、現在は実装されていませんがTypeScript 3.7のリリースまでには対応されそうなものもあります(issue)。以下のサンプルはmasterブランチをビルドして動作を確認しました。
従来の型の絞り込みはこういう感じです。
function func(v: string | number) {
if (typeof v === "string") {
// ここではvはstring型
console.log(v.length);
}
}
この関数ではstring | number
型の変数v
に対してtypeof
演算子を使った条件分岐を書きました。TypeScriptはこれを検知し、if文の中ではv
をstring | number
型ではなくstring
型として扱います。v
が数値である可能性を排除出来たことになりますね。これが型の絞り込みです。
では、Optional Chainingを混ぜてみましょう。
interface HasFoo {
foo: number;
}
const obj: HasFoo | undefined = Math.random() < 0.5 ? { foo: 123 } : undefined;
if (typeof obj?.foo === "number") {
// ここではobjがundefinedの可能性が消えているのでこれはOK
console.log(obj.foo)
}
さっきと同様にHasFoo | undefined
型を持つ変数obj
に対してtypeof obj?.foo === "number"
というチェックを行っています。
実は、このチェックを通ったif文の中ではobj
がundefined
である可能性が消えてHasFoo
型となります。なぜなら、obj
がundefined
だった場合はobj?.foo
は必ずundefined
となり、typeof obj?.foo === "number"
が満たされることはないからです。
他にもif (obj?.foo === 123)
とかif (obj?.foo)
のような判定でも同様に型の絞り込みが行われます。これはたいへん助かりますね。
このように、optional chainingを含んだ条件分岐を行うことでオブジェクトがnullishな値である可能性を消すことができます。
発展:Optional Chainの型推論の実装
これはTypeScriptコンパイラの内部処理に関する話なので、興味がない方は飛ばしても問題ありません。
Optional Chainingの型推論にあたっては、愚直に実装するとうまくいかない点があります。TypeScriptではその点をoptional typeと呼ばれる内部的な型の表現を導入することで乗り越えています。optional typeは「?.
由来のundefined
型」です。基本的には通常のundefined
型と同じ振る舞いをする型であり、通常のundefined
との違いはOptional Chainingの型推論の内部でのみ表れます。
Optional typeのはたらきを理解するために、次の例を見てみましょう。
interface HasFoo {
foo: number;
}
interface HasFoo2 {
foo?: number;
}
const obj: HasFoo | undefined = Math.random() < 0.5 ? { foo: 123 } : undefined;
const obj2: HasFoo2 | undefined = Math.random() < 0.5 ? { foo: 123 } : undefined;
// obj?.foo と obj2?.foo はどちらも number | undefined 型
const v1: number | undefined = obj?.foo;
const v2: number | undefined = obj2?.foo;
// これはOK
obj?.foo.toFixed(2)
// これは型エラー
obj2?.foo.toFixed(2)
HasFoo
型とHasFoo2
型はどちらもfoo
プロパティを持つオブジェクトですが、foo
がundefined
の可能性があるかどうかという違いがあります。
その違いはobj?.foo
とかobj2?.foo
では可視化されません。この2つはどちらもnumber | undefined
型を持ちます。
しかし、obj?.foo.toFixed(2)
とobj2?.foo.toFixed(2)
のようにさらにチェーンを繋げるとその違いが表れ、前者はコンパイルが通る一方で後者は型エラーとなります。まずこの理由を理解しましょう。
まず前者を考えます。obj
はundefined
の場合とHasFoo
の場合があり、前者の場合はobj?.foo.toFixed(2)
は即座にundefined
となり終了します。obj
がHasFoo
だった場合は、obj?.foo
がnumber
となり、よってobj?.foo.toFixed(2)
の呼び出しは可能です。
次にobj2
の場合を考えてみます。obj2
がundefined
の場合は先ほどと同様です。一方でobj2
がHasFoo2
の場合ですが、HasFoo2
自体のfoo
がundefined
の可能性を秘めているためobj2?.foo
は依然としてnumber | undefined
型です。これにより、obj2?.foo.toFixed(2)
はobj2?.foo
がundefined
の可能性があるため型エラーとなります。
ここで問題となるのは、obj?.foo
とobj2?.foo
の型だけを見るとどちらもnumber | undefined
型となってしまい、それに対する.toFixed(2)
呼び出しを許可していいのかどうか判断できないという点です。obj?.foo.toFixed(2)
という一連の式を見た場合、コンパイラはまずobj?.foo
部分の型推論を行い、その結果に対して.toFixed(2)
の推論を行います。少なくとも今のTypeScriptの実装では、(式).toFixed(2)
というノードの型推論を行うときに得られる(式)
部分の情報はその型のみです。しかし、上で見たようにそれだけだと情報が不足しており適切な判断ができないというわけです。
この問題に対するワークアラウンドとして、コンパイラの内部でoptional typeが導入されました。これを便宜上optional
と書くことにします(実際のTypeScriptプログラムでそう書けるわけではありません)。
具体的には、?.
由来のundefinedに対してはoptional
型を付与します。そして、Optional Chain内部の型推論においてはoptional
型を無視してプロパティアクセス等が可能かどうか判断します。
すなわち、obj?.foo
の型推論結果はnumber | optional
となります。これに対して.toFixed(2)
のメソッド呼び出しの型推論を行うときは、一時的にoptional
を無視してnumber
として扱います。そうするとメソッド呼び出しは許可され、結果はstring
となります。optional
は伝播するので結果にoptional
を戻し、obj?.foo.toFixed(2)
の型はstring | optional
となります。
一方、HasFoo2
の場合はfoo
プロパティがもともとnumber | undefined
でした。これにより、obj2?.foo
の型はnumber | undefined | optional
となります。これに対する.toFixed(2)
呼び出しを考えると、optional
を取り除いても依然としてundefined
型の可能性が残っているため型エラーとなります。
このようにして上記の2種類の式を区別しているのです。型の世界で話を終わらせるために内部的に特殊な型を導入するといいうことはTypeScriptのコンパイラでは結構行われています。
以上でTypeScriptの話は終わりです。
Optional Chainingの歴史
ここからは、Optional Chainingの歴史を見ましょう。この概念がどれだけ昔からあったのか正確に知ることは難しいものの、確認できた限りで一番古いのはGroovyです。また、JavaScriptの文脈からするとCoffeeScriptの影響も大きいと考えられます。
Groovy
JVM言語は苦手分野なのでよく分からないのですが、少なくとも2007年にはすでにGroovyは?.
を持っていたという情報があります。
CoffeeScript
CoffeeScriptのChangelogによれば、?.
は2010年1月5日リリースのCoffeeScript 0.2.0で導入されました。Groovyをはじめとする他の既存の言語を受けたのかどうかはよく分かりませんが、?.
という構文が一致していえることからその可能性は十分にありそうです。
CoffeeScriptはAltJSでありセマンティクスがJavaScriptと同じなので、CoffeeScriptが?.
をどのように実装したのかは非常に興味深いところです。実際のところ、CoffeeScriptは現在のJavaScriptのOptional Chainingの挙動をほぼそのままの形ですでに実装していました。
ただ、先述のように構文はJavaScriptとは微妙に違っています。JavaScriptにおいて?
という記号が条件演算子のせいで扱いにくいものになっていることを考えると、条件演算子を廃して?
をフリーにしたのは英断と言えると感じられます。
ちなみに、CoffeeScriptは?
はnullish関連のいろいろな場面で使われます。例えばfoo?
という式はJavaScriptのfoo != null
に相当します。また、JavaScriptでfoo ?? bar
と書く式もCoffeeScriptではfoo ? bar
と書けます。
初期の議論
これをJavaScriptに入れたいという議論は2013〜2014年ごろからあったようです。メーリングリストでの議論がesdiscuss.orgにまとまっています。他にも何個かスレッドがあり、プロポーザル文書からリンクが貼られています。
読むと分かりますが、初期からすでに?.
という構文が優勢だったようです。obj?[expr]
のようなものも模索されましたがやはり前述の理由でうまくいきません。他の構文の候補やセマンティクスなど一通りの議論がここでなされました。
TC39ミーティング
その後舞台はTC39ミーティングへと移ります。TC39というのはJavaScript (ECMAScript) の仕様を策定する委員会で、仕様をJavaScriptに採用するかどうかはここで決められます。
採用候補の仕様はプロポーザルという形で管理されます。Optional Chainingもひとつのプロポーザルです。
プロポーザルはいくつかのステージに分類されます。ステージは0から4までの5段階あり、ステージ0は有象無象のアイデア、ステージ4は採用決定という段階です。現在Optional Chainingはステージ3です。ステージ3はほぼプロポーザルの内容が固まったので正式採用前だけど実装してみようぜという段階で、ここまで来ると我々が使えるようになり正式採用も近くなります。
ステージの上げ下げはTC39のミーティングによって決定されます。基本的にはステージが上がるかそのままかですが、稀にステージが下げられてしまうこともあります。
ここからは、ステージ上昇を賭けた各ミーティングの様子をざっくり振り返ります。
Stage 1: 2017年1月
Optional ChainingがTC39ミーティングに最初に登場したのは2017年1月の回です。このプロポーザルは当初はNull Propagation Operatorという名前でした。
プロポーザルの最初の関門は、TC39の興味を惹きつけてStage 1に認定されることです。
議事録の初っ端に
All: having a hard time reading the screen
と書いてあって笑いました。実際のスライドを見るとたしかに文字が小さいですね。
結論から言えばStage 1になることができたので終わりよければ全てよしですが。
Stage 1になるためには仕様の詳細まで決まっている必要はありません。実際、Stage 1になるためのスライドでは?.
という演算子のコンセプトのみが述べられています。ただ、スライドとは別に今回の場合は初期から比較的詳細な案が用意されており、よい叩き台となったようです。
全体的に?.
は好評でしたが、?.[ ]
や?.( )
は何か見た目が微妙だし本当に必要だろうかとか、短絡評価のセマンティクスが微妙とか、( )
のあるなしで意味が変わってしまうのが微妙といった議論がありました。
また、ES2015で導入されたオプショナル引数がundefined
のみに対応していることを考えると、?.
がnull
とundefined
に両対応すべきかそれともundefined
に対応すべきかも一考の余地がありそうでした。
とはいえ、これらの内容はStage 1になってから議論しても遅くはありません。ということで、無事にこのプロポーザルはStage 1になりました。今後Stage 2を目指すにあたってはこれらの内容が議論の焦点になります。
2017年7月
次にこのプロポーザルがTC39ミーティングに登場したのは7月です。前回のミーティングで挙げられた課題について回答をまとめ、Stage 2の承認を得ることが目的です。
ここで説明されたセマンティクスはおおよそ最終的なものと同じですが、(a?.b).c
がa?.b.c
と同じになる点が今と違いました。また、代入のサポートをするかどうかもまだ未定という段階でした。
このときのスライドはCoffeeScriptで書かれた既存のコードに対する利用状況調査などが含まれており面白いです。
結論としては、このミーティングでプロポーザルがStage 2に進むことはできませんでした。特に短絡評価周りで混乱が起こり、参加者を納得させられるより明確な説明が必要という結論になりました。次回の宿題ですね。
2018年1月
その2つ後のミーティングでOptional Chainingが再び議題にあがりました。これはStage 2が目標というよりはTC39に意見を聞きたいという意図のほうが強いようです。
今回、なぜか?.
が??.
に変わりました(GitHub上で投票を行ってみた結果のようです)。この場合[ ]
や( )
はobj??[expr]
やfunc??(arg)
となり少し見た目が良くなるので、そちら側に寄せた変更といえます。
そのほかは前回の課題であった短絡評価のセマンティクスの明確化がおもな議題です。多くのスライドや具体例を用いてセマンティクスが説明されました。また、分かりやすくする目的で仕様テキストも大きく書きなおされました。ちゃんと読んでいないのですが以前はNil Referenceというやばそうな概念があったらしく、改訂でそれが消されて現在のものに近くなりました(仕様書についてはあとで解説があります)。
TC39の反応としては?.
でも??.
でもどちらでも良さそうな感じでどちらかに決まることはありませんでした。短絡評価に関してはしっかりと説明したことで納得度が向上したようです。
2018年3月
TC39ミーティングは2〜3ヶ月に1回なので、前回に引き続いての登場です。前回比較的良い手応えを得たので今回はStage 2を目指しての議論となりました。なお、構文は?.
に戻りました。もう一度投票してみたら?.
が優勢になったことと、??
にするとnullish coalescing operatorが??:
になってしまうのが辛かったようです。
しかし、結論としては今回もStage 2に進むことができませんでした。?.[ ]
や?.( )
という構文に対する強烈な反対があり全然話が進まなかったようです。
2018年11月
今回は発表者がこれまでと違うのでスライドの雰囲気が今までと全然違います。一見の価値があるかもしれません。
内容としては、まずオプショナルな関数呼び出しfunc?.()
が消えてしまいました。obj?.[expr]
は依然として残っています。obj?.foo
も含めた三者がセットでなければいけないと主張していた勢力が折れた形です。
これに対する反応はまずまずといったところで、強い賛成も反対も見られない様子です。今回のミーティングでは話が右往左往してあまり進まなかった印象です。
Stage 2: 2019年6月
おまたせしました、約2年に渡った停滞を乗り越えてOptional ChainingがStage 2となったのが今年の6月のことです。つい最近ですね。ちなみに、今回はまた発表者が別の人です。
内容としては、?.()
が復活しました。今回の議論の争点もそこだったらしく、これを入れるか入れないかがひとしきり議論されました。
反対意見もありましたが結局?.()
を含んだままStage 2への移行が決定する形となりました。Optional Chainingに対するコミュニティの強い期待と、構文に関して長い時間をかけて解決策を模索したが何も見つからなかったことへの諦めから、やっとプロポーザルが次の段階に進むことができたという感じです。
ちなみに、Nullish Coalescing (??
演算子)も同じミーティングでStage 2に移行しました。まあ、この2つはセットで扱われるのが自然ですからそんなに不思議ではありません。
Stage 3: 2019年7月
スケジュールの都合で2ヶ月連続となったTC39ミーティング(6月頭と7月終わりなので実際は1ヶ月半くらい間があります)です。前回Stage 2となったOptional Chainingはスピード出世でStage 3となりました。
今回のスライドでは、前回未だに争点となっていた?.()
に関して重点的に説明されています。?.()
のユースケースを集めてその必要性を説く形です。
そして今回の争点もやはり?.()
でした。今回、?.()
の見た目よりはその挙動が争点となったことで議論が白熱しました。123?.()
のようなものもエラーではなく無視されるべきではないかというような話がされました。
色々と議論がありましたが結局周りに説得され、全会一致でStage 3への昇格が決まりました。めでたしめでたし。
その後
プロポーザルがStage 3になると、実装が動き始めます。先述のようにTypeScriptがOptional Chainingの実装に向けて動き、3.7でリリースされます。
また、WebkitもStage 3への昇格直後に動き始め、8月のうちに対応を完了しています。Chrome (V8) も同様に8月のうちに対応しています。Chromeは9月に公開されたGoogle Chrome 78 ベータ版にフラグ付きですがOptional Chainingのサポートが含まれています。
以上がOptional Chainingの歴史でした。Stage 4への昇格が楽しみですね。
Optional Chainingの仕様
最後に、この節ではOptional Chainingを仕様の観点から見ていきます。やや難易度が高いので興味の無い方は飛ばしても大丈夫です(あと残っているのはまとめだけですが)。
なお、仕様書という場合は以下の文書を指しています。
OptionalExpression
さて、仕様の観点から見ると、このプロポーザルは文法にOptionalExpressionという非終端記号を追加するものです。OptionalExpressionの定義を仕様から引用します(画像)。
読むと分かるように、OptionalExpressionは左側の部分(MemberExpression, CallExpression, OptionalExpressionのいずれか)にOptionalChainがくっついた形になっています。このOptionalChainという非終端記号が上で説明したオプショナルチェーンにちょうど対応します。
例えばobj?.foo.bar
が文法上どう解釈されるかを表で表すと、このようになります。
OptionalExpression | |||||
MemberExpression PrimaryExpression IdentifierReference Identifier IdentifierName |
OptionalChain | ||||
OptionalChain | IdentifierName | ||||
IdentifierName | |||||
obj |
?. |
foo |
. |
bar |
ポイントは、先に説明したように?.foo.bar
という部分全体がひとつのOptionalChainとして扱われていることです。OptionalChainの中身は左再帰で定義されており、先頭が?.
であとは通常の.
や[ ]
、そしてArguments(これは関数呼び出しの( )
を表しています)が続くことで構成されていることが分かります。
また、この文法から「チェーンの一部を括弧で囲むと意味が変わる」ということも読み取れます。括弧で囲まれた式はParenthesizedExpressionになりますが、これはPrimaryExpressionの一種であり直接OptionalChainになることはできません。
例えば、(obj?.foo).bar
の解釈は以下に定まります。
MemberExpression | ||||||
MemberExpression PrimaryExpression ParnthesizedExpression |
IdentifierName | |||||
Expression (中略) OptionalExpression |
||||||
MemberExpression (中略) IdentifierName |
OptionalChain (内訳は省略) |
|||||
( |
obj |
?. |
foo |
) |
. |
bar |
obj?.foo.bar
と(obj?.foo).bar
では構文木の形が大きく違うことが分かりますね。Optional Chainingの構文が前述のような木の形で定義される理由は、主に前述のセマンティクスを説明しやすいからです(逆に、構文上の表現に合致するようにセマンティクスを決めたとも言えます)。特に、括弧でオプショナルチェーンを切ることができるという構文上の事象とセマンティクスがちょうど合致していて扱いやすいものになっています。
OptionalExpressionとタグ付きテンプレートリテラル
注意深い読者の方は、上で引用されたOptionalExpressionを見てあることに気づいたかもしれません。OptionalExpressionの定義にこのようなものが混ざっています。
これはすなわち、func?.`abc`
のような式がOptionalExpressionとして解釈されるということです。こればfunc`abc`
というタグ付きテンプレートリテラルのオプショナル版に見えます。
OptionalExpression | ||||
MemberExpression | OptionalChain | |||
TemplateLiteral NoSubstitutionTemplate |
||||
TemplateCharacters | ||||
func |
?. |
` |
abc |
` |
しかし、先述の通り、Optional Chainingはタグ付きテンプレートリテラルをサポートしていないはずです。実際のところこう書いた場合はエラーが発生します。このことは、仕様書の以下の場所に書かれています(画像で引用)。
要するに、func.`abc`
のような形は定義してあるけど実際には文法エラーになるよということです。また、ここに書いてある通りobj?.func`abc`
のような形も同様です。
わざわざ定義してから構文エラーにしなくても、最初から定義しなければいいと思われるかもしれません。そうしなかった理由は上記引用中にNOTEとして書かれています。これについて少し解説します。
理由を一言で説明すると、ここで明示的に定義しておかないと、かの悪名高きセミコロン自動挿入によって別の解釈が可能になってしまうからです。仕様書に書かれている例を引用します。
a?.b
`c`
現在の定義では、これは2行でひとつの式として解釈されます。すなわち、a?.b`c`
というOptionalExpressionとして解釈され、上記の規則により構文エラーとなります。
一方で、これをOptionalExpressionとして扱う構文規則がなかった場合を考えてみます。まず、改行がない場合はa?.b`c`
というコードは解釈することができずにやはり構文エラーとなります。タグ付きテンプレートリテラルはMemberExpression TemplateLiteral
という並びによって定義されますが、上で見たようにOptionalExpressionはMemberExpressionの一種ではないからです。
これを踏まえて、上の2行のプログラムを見てみます。1行目のa?.b
を読み終わって2行目に入り`
を読んだ時点で構文エラーが発生することになります。ここで自動セミコロン挿入が発動します。ここで適用されるルールをざっくり言うと「改行の直後でコードが解釈不能な状況に陥ったら直前にセミコロンを挿入してみる」というものです。これにより、上のプログラムの解釈はこれと同じになります。
a?.b;
`c`
この解釈においては大まかに分けて2つの問題があります。ひとつは、将来的な拡張が困難になる点です。現在a?.b`c`
がサポートされていないのはユースケースの欠如が理由とされています。つまり、関数が存在するときだけタグ付き関数呼び出しが出来るという機能の需要が無さそうなのです。もし将来的に需要が発見されたらこの機能をサポートする道もあるわけですが、上記のプログラムに別の解釈が与えられてしまうと将来的にその解釈を上書きすることができなくなってしまいます。それを避けるために、わざと文法エラーにすることで将来的な拡張の余地を残しているのです。
もうひとつの問題は通常の(オプショナルでない)タグ付きテンプレートリテラルとの対称性です。現在、以下のプログラムはa.b`c`
として解釈されます。
a.b
`c`
a.b
をa?.b
に変えるといきなり解釈が変わって文が2つになるというのはたいへん微妙だし非直感的ですね。思わぬミスが発生するのを避けるために.
と?.
の場合で解釈が大きく変わらないようになっています。
OptionalExpressionとLeftHandSideExpression
OptionalExpressionの定義をもう一度振り返りましょう。
一番下にすこし気になることが書いてあります。OptionalExpressionは、NewExpressionやCallExpressionと並んで、LeftHandSideExpressionの一種であるとされています。
LeftHandSideExpressionとは何の左側のことを指しているのでしょうか。実は、これは=
の左側です。これが意味することは、obj?.foo = 123
のような式が構文上可能であるということです。
AssignmentExpression | ||||
LeftHandSideExpression OptionalExpression (詳細は省略) |
AssignmentExpression (中略) IdentifierName |
|||
obj |
?. |
foo |
= |
123 |
やはり、これも構文上認められるとはいえ実際には文法エラー扱いになります。このことは仕様書の既存のEarly Error定義12.15.1 Static Semantics: Early Errorsに定義されています(該当部分を以下に引用)。
It is an early Syntax Error if LeftHandSideExpression is neither an ObjectLiteral nor an ArrayLiteral and AssignmentTargetType of LeftHandSideExpression is strict.
実はプロポーザル文書のほうを見るとAssignmentTargetTypeではなくIsSimpleAssignmentTargetというものが定義されています。ちゃんと追っていないのですが、仕様執筆時にちょうどこのあたりの改稿が議論されていて齟齬があったようです。多分そのうち直るでしょう。
まとめ
お疲れ様でした。この記事ではOptional Chainingの機能面を解説し、さらにStage 3に上がるまでの歴史と仕様の中身にも少し触れました。
機能面では短絡評価をちゃんと理解することがポイントとなります。一度理解すれば大したことはありませんのでぜひ今のうちに予習しておきましょう。
歴史に関しては、当初はセマンティクスで少し揉めたもののすぐに沈静化し、?.( )
という構文が激しい反発を呼んで2年ものあいだ停滞したことがお分かりになったと思います。TC39の面々が年単位で考えぬいてベストだと判断された(というかは妥協して受け入れるまでに年単位の時間がかかった)?.( )
構文ですから、素人考えで構文にああだこうだと文句を付けるのはあまり意味が無いことがお分かりでしょう。
仕様に関しては構文がどのように扱われているのかなどに興味がある方向けに解説しました。よく分からなくてもOptional Chainingの使用に問題はないと思いますのでご安心ください。
この記事執筆時にはStage 3プロポーザルであるOptional Chainingですが、多分ES2020か遅くともES2021くらいでStage 4に上がるものと思われます。楽しみですね。この記事を読んだみなさんはOptional Chainingに関して大抵のことは聞かれても答えられることでしょう。ぜひ周りにOptional Chainingを布教しましょう。
追記
- (2019年10月21日)調査不足により
?.
を持つ言語の中でCoffeeScriptが一番古いと思わせる書き方がされていましたが、実際のところGroovyのほうがさらに何年も前から?.
を持っていました。お詫びして訂正します。
-
おおよそというのは、
obj
を評価するのは1回だけであるとか、document.all
との兼ね合いといったことを指しています。 ↩ -
obj
がnull
のときはobj?.foo
がnull
になってほしいという意見もありそうですが、このときの結果がundefined
である理由付けは一応あります。それは「obj?.foo
はobj
についての情報を得るためのものではなくfoo
の情報を得るためのものである。null
もundefined
もどちらもfoo
を持たないという点で同じなのだから、結果はundefined
に統一してobj
の情報が伝播しないようにすべきである」というものです。個人的にはまあ一理あると思っています。また、JavaScript本体の言語仕様はそもそもnull
よりもundefined
に偏重しているため(DOMは逆にnull
に寄っていますが)、undefined
に統一されるのもまあ妥当に思えます。 ↩ -
厳密に言えば、ゲッタがエラーを発生させた場合や
Proxy
の場合など、プロパティアクセスに起因してエラーになる可能性は他にもあります。とはいっても、今はそういう話をしているのではないことはお分かりですよね。 ↩ -
obj[expr]
はobj.get(expr)
の糖衣構文。 ↩ -
never
型のボトム型的な性質を考えると「never
型に対しては任意のプロパティアクセスを許可してその結果もnever
型になる」というような推論も理論的には可能だと思いますが、ユーザーに対する利便性を考えて今の仕様になっているのだと思われます。 ↩