altJS、すなわち、JavaScriptの代わりとなる言語の筆頭であるTypeScript。そのTypeScriptは、言語名が示す通り、JavaScriptにType、つまり、型の概念を持ち込んだものです。本連載では、このTypeScriptのType(型)に関して、さまざまな方向から紹介していきます。前回は、既存のデータ型を組み合わせて新たなデータ型を定義できる型エイリアスを紹介しました。今回は、データ型を定義できるもうひとつの仕組みとして、インタフェースについてあれこれ紹介していきます。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
前回の最後に、オブジェクトリテラルを紹介しました。その際、型定義としてtypeキーワードを利用しました。型定義には、もうひとつ、インタフェースという仕組みも利用できます。
前回のリスト19のBMIDataを、インタフェースを利用して定義すると、リスト1のコードとなります。
interface BMIData { // (1) name: string; height: number; weight: number; age?: number; } const taro: BMIData = { : };
リスト1の(1)が、インタフェースを定義している部分です。構文としてまとめると、次のようになります。
[構文]インタフェースの定義 interface インタフェース名 { プロパティ名: データ型; : }
型エイリアスと違い、インタフェースは定義そのものなので、「=」による代入式ではありません。interfaceキーワードにインタフェース名を続けて、すぐに定義の波かっこブロックを記述します。また、代入式ではないので、閉じ波かっこの次のセミコロンは不要です。
波かっこブロック内では、前回紹介した内容と同様に、「プロパティ名: データ型;」と記述します。
なお、このようにインタフェースを型定義として利用する方法を、型注釈インタフェースといいます。というのは、型定義とは別の使い方があるからで、それについては後で紹介します。
型エイリアスと型注釈インタフェースと、同じ機能のものが2個あるように思えますが、もちろん違いがあります。インタフェースは、あくまでオブジェクトの型指定に利用するためのものなので、前回の前半で紹介したような、既存の型をもとに型定義するような状況では利用できません。
例えばリスト2は、前回のリスト2で定義したNumStrをインタフェースで定義しようとした例です。しかし、こちらは図1のエラーとなります。このような型定義には、型エイリアスを利用します。
interface NumStr { number|string }
逆に、インタフェースにはできて型エイリアスにはできないものに、プロパティの自動追加機能があります。例えば、リスト3のコードを記述したとします。
interface BMIData { // (1) name: string; height: number; weight: number; } interface BMIData { // (2) age: number; }
リスト3の(1)におけるインタフェースBMIDataの定義は、リスト1とほぼ同じであり、ageプロパティがないだけです。続けて、(2)で同じBMIDataという名称でインタフェースを定義しています。そして、そのコードでageを定義しています。この場合、名称が重なるからといってエラーとなるのではなく、プロパティが自動的に追加され、結果的にBMIDataインタフェースは、リスト4の定義と同じ意味になります。
interface BMIData { name: string; height: number; weight: number; age: number; }
そのため、このBMIData型のオブジェクトではageプロパティは必須項目となり、記述しなければ図2のエラーとなります。
このようなプロパティの自動追加機能は、型エイリアスにはありません。試しに、リスト3と同じようなコードをtypeキーワードを利用して記述すると、図3のエラーとなります。
リスト3の(2)では、(1)に存在しないプロパティを指定することで、プロパティの追加となりました。これをリスト5のように、同じプロパティで別のデータ型を記述すると上書き扱いとなり、エラーとなるので注意してください。
interface BMIData { weight: boolean; }
前項で紹介したインタフェースへのプロパティの自動追加は、同じインタフェース名を指定することで可能でした。一方、全く違うインタフェースを基に、新たなプロパティを明示的に追加することも可能です。つまり、インタフェースの拡張です。その場合は、リスト6の(2)のようにextendsキーワードを利用します。
interface BMIData { // (1) name: string; height: number; weight: number; } interface BMIDataWithAge extends BMIData { // (2) age: number; }
リスト6の(1)で用意したageプロパティのないBMIDataをもとにして、(2)のように「extends BMIData」とすることで、ageプロパティを追加したインタフェースを別に用意できます。こちらの方法では、リスト3のプロパティの自動追加とは違い、BMIDataとBMIDataWithAgeが別インタフェースとして存在しているので、リスト7のようにそれぞれのオブジェクトを用意できます。
const taro: BMIData = { : }; const jiro: BMIDataWithAge = { : };
この場合、taroはageが定義されていないBMIDataを型指定としているので、ageプロパティを記述すると図4のようにエラーとなります。
一方、jiroはageが定義されているBMIDataWithAgeを型指定としているので、逆にageプロパティを記述しないと図5のようにエラーとなります。
このようなインタフェースの拡張を利用すると、それぞれを別の型として利用できるようになります。
なお、このインタフェースの拡張と同じ仕組みは、型エイリアスにもあります。その場合は、リスト8の(1)のように「&」演算子を利用します。
type BMIData = { name: string; height: number; weight: number; }; type BMIDataWithAge = BMIData & { // (1) age: number; };
この「&」演算子を利用した拡張と、extendsによるインターフェースの拡張とを比べた場合、後者をお勧めします。というのも、extendsキーワードは、第6回のクラスの継承で紹介していますし、その際紹介したものと同じ要領で記述できるからです。
前項末の補足と同じような内容として、子インタフェースで親インタフェースのプロパティを、データ違いで再定義することはエラーとなります。つまり、プロパティ定義を丸々上書きする場合です。
例えば、リスト6の(1)にあるBMIDataをextendsしたリスト9のBMIDataWeightBooを定義しようとすると、weightのデータ型がBMIDataと互換性がないためエラーとなります。
interface BMIDataWeightBoo extends BMIData { weight: boolean; }
ただし、これは親のデータ型を狭める形ならばエラーとなりません。例えば、BMIDataのweightのデータ型が、もともとnumber|booleanと定義されているならば、リスト9はエラーとなりません。これは、プロパティ定義をまるまる上書きした形にならないからです。
前節で紹介したインタフェースは、プロパティを宣言しているだけでした。一方、インタフェースには、他にもさまざまな定義を型として記述できます。これらはシグネチャといわれます。本節では、プロパティ以外のシグネチャを、順に紹介していきます。
ここまで紹介してきたように、インタフェースはオブジェクトの構造を型として定義するためのものです。そのうちプロパティシグネチャは、これまでの例に示した「プロパティ名: データ型;」というように、各プロパティの名称とそのデータ型を記述することで定義とします。
ここで、このデータ型が全て同一の場合は、リスト10の(1)にあるようなシグネチャのインタフェースを定義できます。これを、インデックスシグネチャといいます。
interface NumberMap { [key: string]: number; // (1) } const numbers: NumberMap = {}; // (2) numbers["a"] = 35; // (3) numbers["b"] = 42; // (4) numbers.c = 38; // (5)
(1)のインデックスシグネチャを構文としてまとめると次のようになります。
[構文]インデックスシグネチャ [key: プロパティ名のデータ型]: プロパティのデータ型;
少し補足しておきます。「key」という部分には、任意の文字列を記述できます。通常は、上記の構文のようにkeyあるいはindexとします。続く「:」(コロン)の次にプロパティ名のデータ型を記述し、それら全体を「[]」で囲みます。このプロパティ名のデータ型は、number型かstring型のみ利用できます。名称のデータ型というのを考えると理解できます。
このような、インデックスシグネチャのインタフェースをデータ型とするオブジェクトは、連想配列のような働きをします。そのため、リスト10の(2)にあるように空のオブジェクトを用意し、そのオブジェクトに(3)と(4)のように次々とデータを格納することができるようになります。ただし、その実態は、(3)でプロパティaが、(4)でプロパティbが追加されている構造です。通常のプロパティアクセスのように、「.」(ドット)を利用して(5)の記述も可能です。
これまで紹介したような、プロパティシグネチャが定義されたインタフェースを型指定したオブジェクトでは、そのプロパティの名称も個数も定義通りでないとエラーとなります。これに対してインデックスシグネチャは、リスト10の例でも分かるように、個数を自由に増やせます。一方で、データ型は指定のもの以外は利用できません。リスト10ではnumber型を指定しているため、「こんにちは」のような文字列を代入しようとすると、図6のエラーとなります。
コールシグネチャは、関数の型定義です。その役割は、前回のリスト6の(1)で紹介した、型エイリアスによる関数の型定義と同じものです。ただし、定義方法が違い、リスト11のようになります。
interface Calc2RandFunc { (rand1: number, rand2: number): number; }
インタフェース定義の波かっこブロック内に関数の型定義を記述しますが、戻り値のデータ型記述が「=>」ではなく「:」(コロン)になっている点が違います。その違いだけ理解しておけば、使い方は型エイリアスで定義されたものと同じです。
コールシグネチャと似て非なるものがメソッドシグネチャです。こちらは、リスト12の(1)のような記述です。
interface Calc2Rand { name: string; calc(rand1: number, rand2: number): number; // (1) } const c2r: Calc2Rand = { // (2) name: "乱数の掛け算", calc: function(rand1: number, rand2: number): number { // (3) return rand1 * rand2; } }; const rand1 = Math.round(Math.random() * 10); const rand2 = Math.round(Math.random() * 10); const ans = c2r.calc(rand1, rand2); // (4) console.log(`${c2r.name}の実行結果: ${ans}`);
リスト12の(1)は関数の型定義、すなわちコールシグネチャのように見えますが、「calc」という名称が定義されています。これがメソッドシグネチャです。そして(2)のように、このメソッドシグネチャが定義されたインタフェースをオブジェクトの型定義として指定した場合、(3)のように、その名称の関数をプロパティとして記述する必要があります。
これは裏を返せば、このCalc2Rand型のオブジェクトは必ずcalcメソッドを実装していることを意味し、(4)のように安心してそのメソッドを呼び出すことができるようになります。
インタフェースには、本節で紹介したシグネチャ以外に、もうひとつコンストラクタシグネチャがあります。コードとしては、リスト13のようにnewキーワードを使ったシグネチャです。
interface Calc2Rand { new (rand1: number, rand2: number): Calcs; }
ただし、このシグネチャは、JavaScriptにある既存のAPIとの互換性維持のために導入されたシグネチャであり、実際に利用する場面というのはほとんどありません。そのため、今回は紹介程度にとどめておきます。
前項で紹介したメソッドシグネチャについて、メソッドという名称から第5回で紹介したクラス構文のメソッドを思い浮かべた方もいるかもしれません。実は、インタフェースはオブジェクトだけではなく、クラス定義にも適用することもできます。最後に、その仕組みを見ていきましょう。
インタフェースをクラス定義に適用する場合は、リスト14の(1)のようにimplementsキーワードを使います。
class Multiply2Rand implements Calc2Rand { // (1) name = "乱数の掛け算"; calc(rand1: number, rand2: number): number { return rand1 * rand2; } } const c2r = new Multiply2Rand(); // (2) :
リスト14の(1)でimplementsの次に記述しているCalc2Randは、リスト12のCalc2Randインタフェースそのものです。この、インタフェースをクラス定義に適用することを、インタフェースを「実装する」といいます。そして、Calc2Randインタフェースを実装したクラスであるMultiply2Randは、Calc2Randインタフェースに定義されたプロパティやメソッドを、全てメンバとして定義する必要があります。もちろん、それ以外のメンバも定義できますが、インタフェースに定義されているメンバを記述し忘れると、図7のエラーとなります。
なお、インタフェースを実装したとはいえクラスはクラスなので、利用する場合はリスト14の(2)のようにnewして利用することになります。
JavaやPHPといった言語にもインタフェースは存在します。その使い方は、ここで紹介したimplementsが基本となり、今回の前半で紹介した型注釈インタフェースとしての使い方はありません。
インタフェースを実装したクラスを作成する場合、複数のインタフェースを指定することもできます。その場合は、リスト15のようにインタフェースをカンマ区切りで並べます。
class Multiply2Rand implements Calc2Rand, ShowName { : }
ただし、このように複数指定した場合は、各インタフェースに定義されたプロパティやメソッドを、全て定義する必要があります。
TypeScriptのTypeに注目し、あれこれ紹介する本連載の第10回はいかがでしたか?
今回は、オブジェクトのデータ型を定義できる仕組みとして、インタフェースを紹介しました。
次回は、本連載最後のテーマとして、型操作に関するあれこれを紹介します。
WINGSプロジェクト
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティー(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手掛ける。2021年10月時点での登録メンバーは55人で、現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
・サーバーサイド技術の学び舎 - WINGS(https://wings.msn.to/)
・RSS(https://wings.msn.to/contents/rss.php)
・Twitter: @yyamada(https://twitter.com/yyamada)
・Facebook(https://www.facebook.com/WINGSProject)
Copyright © ITmedia, Inc. All Rights Reserved.