制御フローと型の絞り込み
本連載は、TypeScriptのバージョン3から5.2までのアップデート内容を、テーマごとにバージョン横断で紹介する連載です。第4回である今回紹介するのは、型の絞り込みに関する変更点です。まず、その型の絞り込みの確認から話を始めていきます。
型の絞り込みとは
例えば、前回登場したPersonalBaseData型を値の型、数値をキーの型とするMapオブジェクトを用意し、リスト1のようなコードを考えたとします。
const list = new Map<number, PersonalBaseData>(); : const key = 22454; const item = list.get(key); const name = item.name; // (1)
このコードは、(1)で図1のエラーとなります。このエラーメッセージの通り、Mapオブジェクトからget()した要素は、引数のキーのデータによっては取得対象の値が存在せず、undefinedとなる可能性があるからです。そして、このundefinedな変数のプロパティへのアクセスはエラーを招くため、TypeScriptでは事前にエラー表示となります。
実際、get()とした値を格納した変数(リスト1ではitem)のデータ型をVS Codeで表示させると、図2のように、PersonalBaseDataとundefinedとのユニオン型となっています。
このエラーを解決するために、リスト1の(1)の部分は、リスト2のようにundefinedかどうかのチェックを行います。
if(item != undefined) { const name = item.name; }
そして、TypeScriptでは、このようにundefinedの可能性を排除した条件ブロック内では、その変数(リスト2ではitem)は、図3のように、undefinedの型がなくなり、単なるPersonalBaseData型となっています。
このように、if-else if-elseやswitchの条件分岐構文において、変数の型チェックを行うことを、「制御フロー分析(Control Flow Analysis)」といいます。そして、その制御フロー分析内で、条件に応じて型の可能性が排除されていくことを、「型の絞り込み(Type Narrowing)」といいます。また、型の絞り込みで行う条件部分(リスト2ならば「item != undefined」の部分)を「型ガード(Type Guards)」といいます。
型ガードの変数化
この型ガード、すなわち、型の絞り込み条件は、()内に記述して初めて、制御フローブロック内で型の絞り込みが有効になっていました。これが、変数化しても絞り込みが有効になるようにバージョン4.4で変更されています。これを利用すると、リスト2は、リスト3のようなコードになります。
: const item = list.get(key); const isNotUndefined = (item != undefined); // (1) if(isNotUndefined) { // (2) const name = item.name; // (3) }
リスト3では、(1)で型ガードをisNotUndefinedと変数化しており、その変数isNotUndefinedを(2)の条件としています。それでも、型の絞り込みは行われ、(3)ではエラーが発生しません。
inによる型ガード
型ガードを行う場合、「!= undefined/null」の他に、JavaScript由来のtypeof演算子やinstanceof演算子を利用した型チェックコードも、もちろん型ガードとして利用できます。ただし、typeof演算子は、プリミティブ型のチェックは行えますが、オブジェクトは単にobjectとしてチェックできるだけで、どのオブジェクトかまではチェックできません。また、instanceofはオブジェクトをnewしたものとの比較となります。ということは、オブジェクトリテラルの型チェックはできません。
例えば、前回登場したPersonalBaseData型かPersonalBirthData型かの型ガードを行う場合は、typeofやinstanceofは使えず、代わりに、inを使います。inもJavaScript由来の演算子であり、そのオブジェクトに特定のプロパティが含まれているかをチェックする演算子です。これを利用すれば、リスト4のような制御フロー分析コードが記述できます。
function show(person: PersonalBaseData | PersonalBirthData): void { const name = person.name; // (1) if("phone" in person) { // (2) const phone = person.phone; // (3) : } else { // (4) const birth = person.birth; // (5) : } }
リスト4の関数show()の引数personはPersonalBaseData型かPersonalBirthData型かのどちらかです。そのどちらもnameプロパティは存在しているので、(1)のコードは問題なく動作します。一方、(3)のようなphoneプロパティへのアクセスはPersonalBaseData型でなければエラーとなります。同様に、(5)のようなbirthプロパティへのアクセスはPersonalBirthData型でなければなりません。そこで、(2)や(4)のように制御フロー分析を行って、それぞれのブロック内で型の絞り込みが行われた上でアクセスするようにしています。
その条件として利用しているのが、(2)のinです。(2)では、phoneプロパティがpersonに含まれているかを型ガード(型の絞り込み条件)としています。
未定義プロパティへのアクセスがinによって可能に
このinによる型ガードの仕組みが、バージョン4.9でより強力になりました。例えば、あるサーバAPIエンドポイントから取得したJSONデータを表すインターフェースとしてリスト5のReturnJSONを考えます。statusはサーバ側の処理が成功したかどうかを表す数値として1(成功)か0(失敗)が格納されています。そして、その失敗か成功かでresultのデータ内容が変わってくるため、unknown型となっています。
interface ReturnJSON { status: number; result: unknown; }
ここで、statusが1、つまり成功した場合、resultに格納されたオブジェクトのmsgプロパティを取得するコードを考えます。そのような関数をextractResult()としたならば、これは、例えば、リスト6のようなコードとなります。
function extractResult(returnJSON: ReturnJSON): string { let result = "通信失敗"; if(returnJSON.status == 1) { let result = "データ取得失敗"; if(returnJSON.result != null && typeof returnJSON.result == "object" && "msg" in returnJSON.result && typeof returnJSON.result.msg == "string") { // (1) result = returnJSON.result.msg; // (2) } } return result; }
リスト6で注目するのは、(2)です。ReturnJSON型のreturnJSONのresultプロパティは、リスト5の通り、unknown型です。となると、そのプロパティのmsgへのアクセスは、本来エラーとなるはずですが、リスト6ではなりません。その種明かしが、(1)の条件(型ガード)の「"msg" in returnJSON.result」の部分です。この条件に合致した時点で、resultプロパティ内にはmsgプロパティは存在すると判断し、図4のようにプロパティmsgが存在するオブジェクトとして扱われるようになります。