みなさん、Optional Chaining使ってますか? 私は先日出たTypeScript 3.7 Betaを小さいプロジェクトに導入して使ってみました。これはとても快適ですね。
例によって、Optional ChaningもECMAScriptに対するプロポーザルの一つです。つまり、もうすぐ入りそうなJavaScriptの新機能です。プロポーザルはたくさんの種類がありますが、その中でもOptional Chainingはその高い有用性からこれまで多くの注目を集めてきました。Optional Chainingは2019年6月のTC39ミーティングでStage 3に上昇し、いよいよ正式採用が近く期待も高まってきたところです。TypeScript 3.7にも導入されたため、TypeScriptユーザーの方々は11月上旬に正式リリースが予定されているTypeScript 3.7を今か今かと待ち構えていることでしょう(筆者のようにフライングしてBetaで使い始めている人もいるかもしれません)。また、Babelのプラグインも前々から存在したため熱心な方々はもう使っているかもしれません。
となると、巷に、あるいはこのQiitaにもOptional Chaningに関する記事がいくつもあります。そこで自分もOptional Chainingの解説がしたくなってできたのがこの記事です。
この記事を読めばOptional Chaningの全てがわかります。(強気)
なお、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な値を適切に処理することができるケースが増えるでしょう。これはとてもうれしい点です。
プロポーザルによれば、?.に類似の構文は以前からC#, Swift, CoffeeScriptなどに存在していました。この中ではCoffeeScriptが最も古く、AltJSであるということもあり今回のプロポーザルに直接的な影響を与えているといえるでしょう。
3種類の構文
厳密には、今回のプロポーザルで?.に関連する構文が3種類追加されます。
obj?.fooobj?.[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の存在です。どちらも?を使っていることがあり、両方の構文があるとたいへん紛らわしくなります(:があるので多分文法が曖昧になるというわけではなさそうですが)。
プロポーザル文書に載っている例を引用します。
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 Chaningの開始であることが確定します。
一般に長く先読みするほどパースが大変になりますが、まあ1文字先読みくらいなら許容範囲です。
以上がOptional Chainingの基本でした。おおよそプロポーザルの文書に書いてあることを説明した感じです。この文書も分かりやすく書かれていますので一度目を通してみてもよいかもしれません。
他の言語との比較
CoffeeScriptを始めとして、Optional Chainingに類似の言語機能をすでに持っているプログラミング言語はいくつか存在します。ここでは、他の言語とJavaScriptのOptional Chainingとの違いや共通点を明らかにします。
以下の言語はプロポーザルの文書に言及があったものを列挙しています。他にこんな言語にもあるよという情報は大歓迎です。
CoffeeScript
一番手はCoffeeScriptです。CoffeeScriptではこの機能はExistential Operatorと呼ばれ、Optional Chainingの3種類の構文は以下で表現されます。
obj?.fooobj?[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#にも条件演算子? :があるはずですが、前述の問題にも関わらずobj?[expr]の形が採用されています。その理由は調べられていませんが、まあ仕様よりも実装が先行する言語であることや主にコンパイルして使う言語であることなど、事情の違いがあるのでしょう。
短絡評価周りの挙動も、基本的に今回解説した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)4のようにするか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 Preson型で表現します。
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型に対するプロパティアクセスは許可されません5。これが型エラーの原因です。ここで言いたいことは、エラーメッセージに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の歴史を見ましょう。この概念がどれだけ昔からあったのか正確に知ることは難しいものの、JavaScriptの文脈からするとまず語るべきはCoffeeScriptでしょう。CoffeeScriptでは?.はExistential Operatorと呼ばれています。
CoffeeScript
CoffeeScriptのChangelogによれば、?.は2010年1月5日リリースのCoffeeScript 0.2.0で導入されました。
紆余曲折を経て、JavaScriptのOptional Chainingの挙動はこのときのCoffeeScriptのものに非常に近くなっています。さすが?.の原点(推測)ですね。
ただ、先述のように構文は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を布教しましょう。
-
おおよそというのは、
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型になる」というような推論も理論的には可能だと思いますが、ユーザーに対する利便性を考えて今の仕様になっているのだと思われます。 ↩


