altJS、すなわち、JavaScriptの代わりとなる言語の筆頭である「TypeScript」。TypeScriptという言語名が示す通り、JavaScriptに「Type」、つまり、型の概念を持ち込んだものです。本連載では、このTypeScriptの型に関して、さまざまな方向から紹介していきます。連載1回目の前回は、基本中の基本に当たる、型指定にまつわるあれこれを紹介しました。今回はJavaScriptにはない、TypeScript独自の型のあれこれを紹介します。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
配列はTypeScriptオリジナルなものではなく、JavaScript由来のものです。配列のTypeScriptらしい使い方から紹介していきましょう。
JavaScriptで配列変数を用意する場合、リスト1のようなコードを記述します。
const array = [];
このarrayに対して、リスト2のように各要素を格納するコードを記述したとします。
array[0] = 35; // (1) array[1] = "こんにちは"; // (2) array[2] = new Date(); // (3)
(1)では数値を、(2)では文字列を、(3)ではオブジェクトを、それぞれ要素として格納しています。これらのコードは、全て問題なく動作します。
しかし、配列は本来、同種のデータをまとめておくためのものであり、同種のものがまとまっているからこそ、便利になるような仕組みがあります。例えば、JavaScriptの配列には、「reduce()」というメソッドがあり、これを使うと、リスト3のようなコードで簡単に数値配列の合計を計算できます。
const numArray = [1, 2, 3, 4, 5]; const sum = numArray.reduce(function(prev, current, i, arr) { return prev + current; });
ただし、あくまでも配列が数値で構成されている場合にしか使えません。リスト2のarrayのように、数値以外のものを要素とする配列の場合は、当然、合計値になりません。しかも、JavaScriptでは、このようなコードを簡単に防ぐ方法がありません。
一方、TypeScriptには、データ型という仕組みがあるので、配列に対しても、データ型を指定することで、このような問題を防ぐことができます。TypeScriptの配列構文は次の通りです。
[構文1]配列宣言 const 配列変数名: 各要素のデータ型[]
リスト2のarrayを数値型に限定するならば、リスト4のようなコードになります。
const array: number[] = []; array[0] = 35;
これで、配列変数arrayの各要素には数値以外代入できなくなります。もし、「こんにちは」を代入しようとすると、図1のようなエラーになります(以下、「Visual Studio Code」を利用)。
TypeScriptの配列では、型指定のおかげで各要素の型が同一になります。同種のデータをまとめておくもの、という配列本来の働きを実現できるのです。
配列を宣言する構文は、構文1とは異なるものがあります。
[構文2]配列宣言 const 配列変数名: Array<各要素のデータ型>
リスト4を構文2で書き換えると、リスト5になります。
const array: Array<number> = []; array[0] = 35;
通常、1つのプロジェクト内では、構文1と構文2のどちらかに統一しておけばよいでしょう。以降の解説では、より簡素に記述できる構文1を使います。
なお、構文2にある「< >」という記述を、「ジェネリクス」といいます。ジェネリクスに関しては、今後、本連載中で扱う予定です。
JavaScriptでもTypeScriptでも、変数をconstで宣言すると、再代入が不可になります。しかし、配列の各要素の改変では再代入になりません。例えば、リスト6のようなコードは問題なく動作します。
const array1: number[] = [1, 3, 5, 7, 9]; array1[4] = 11; // (1) array1[5] = 13; // (2)
(1)では既存の要素の変更しており、(2)では新しい要素を追加しています。このどちらも可能です。一方、const宣言配列への再代入とは、リスト7のようなコードを指します。
array1 = [2, 4, 6, 8, 10];
この場合は、図2のようなエラーになります。
つまり、const宣言配列は、再代入不可ではありますが、不変(イミュータブル)ではないのです。もし、不変の配列を作成するなら、「readonly」を利用します。例えば、リスト8のようなコードです。
const array2: readonly number[] = [1, 3, 5, 7, 9];
このarray2の各要素を変更しようとすると、図3のエラーになります。
同種のデータをまとめておくものが配列なら、異種のデータをまとめておく方法にはどのようなものがあるのでしょうか。JavaScriptには古くからオブジェクトというものがあります。モダンなJavaScriptでは、クラスの利用も考えられます。オブジェクトもクラスも当然、TypeScriptで利用できます。ここでは、TypeScriptならではの方法として、もっと簡易に異種データをまとめることができる「タプル」を紹介します。
タプルは要素数と各要素の型を限定した配列といえます。具体例を見てみましょう。
例えば、ある人の名前と身長、体重をまとめて1つの変数にしたい場合、オブジェクトやクラスを利用する方法もありますが、タプルならリスト9のようにもっと簡単に、使い捨て感覚で利用できます。
const taro: [string, number, number] = ["太郎", 172.5, 71.5];
右辺のリテラル部分を見ると、型違いのデータが格納された配列のように見えますが、左辺の変数宣言部分のデータ型を記述する部分では、「[ ]」の中に3個のデータ型があります。この記述により、右辺に記述できるリテラルでは、要素が3個と決まり、さらに、インデックス0には文字列、インデックス1と2には数値しか代入できなくなります。
これがタプルです。構文としてまとめると次の通りです。
[構文3]タプル宣言 const タプル変数名: [データ型, データ型, ……]
タプルは要素数と各要素のデータ型が決まってしまうため、それ以外の代入が許されなくなります。例えば、リスト9で、インデックス3、つまり4個目に代入しようとすると、図4のようなエラーになります。
さらに各要素の型もおのおの決まっているので、それに反した値を代入しようとしてもエラーになります。図5は数値指定の要素であるインデックス2、つまり3個目に文字列を代入しようとしてエラーとなった画面です。
タプルも配列同様に、constで宣言していても、不変ではありません。例えば、リスト9の後、taroのインデックス2に対して、数値型であればリスト10のように要素の値を書き換えることができます。
taro[2] = 70.8;
もちろん、リスト11のような値全体の再代入はエラーになります。
taro = ["たろう", 165.5, 68.4];
一方、タプルでも配列同様に、readonlyを付記することで不変なものが作成できます。例えば、リスト12のようなコードです。
const jiro: readonly [string, number, number] = ["次郎", 165.5, 68.4];
このjiroのインデックス2に対して、先のtaroと同じようなコードを記述すると、図6のようにエラーになります。
タプルは配列の応用というイメージでした。一方、次に紹介する「Enum」は、JavaScriptにはなく、TypeScript独自のデータ構造です(もちろん、JavaScriptと違う系列の言語、例えば、JavaなどにはEnumが存在します)。
Enumは列挙型といわれ、いわば定数をまとめておくためのものです。例えば、英語、数学、国語の成績を管理するアプリケーションを作成するとして、その中で、各教科を区別するために、リスト13のような定数を定義したとします。
const ENGLISH = 0; const MATH = 1; const JAPANESE = 2;
アプリケーション中に各教科の点数を登録する関数として、リスト14の(1)のような「addScore()」があるとしたなら、これらの定数を利用して、(2)のようなコードを記述することも可能です。
function addScore(name: string, subject: number, score: number) { // (1) : } addScore("太郎", MATH, 89.4); // (2)
このような定数をまとめておく仕組みとしてEnumを利用すると、コードがスッキリします。Enumの構文は次の通りです。
[構文4]Enum宣言 enum 名前 { 定数名, 定数名, : }
各教科を区別する定数をEnumにするとリスト15の(1)のコードになります。
enum Subjects { // (1) ENGLISH, MATH, JAPANESE } function addScore(name: string, subject: Subjects, score: number) { // (2) : }
このEnumを利用する場合、addScore()の定数を指定する第2引数のデータ型として、Enum型を指定でき、呼び出すコードはリスト16のように記述できます。
addScore("太郎", Subjects.MATH, 89.4);
先の定数宣言とは違い、Enumでは数値を指定する必要がありません。記述順に自動的に0から始まる連番が付与されます。リスト15のSubjectsだと、Subjects.ENGLISHが0、Subjects.MATHが1、Subjects.JAPANESEが2になります。
この番号を明示的に指定することもできます。例えば、リスト17のように、最初の定数の値だけを指定すると、それ以降は、その次の値を定義してくれます。
enum Subjects { ENGLISH = 1, MATH, JAPANESE }
この場合、Subjects.ENGLISHが指定された1、Subjects.MATHが2、Subjects.JAPANESEが3になります。
また、リスト18のように、全ての値を指定することもできます。
enum Subjects { ENGLISH = 10, MATH = 20, JAPANESE = 30 }
定数値として、リスト19のように文字列を割り当てることもできます。
enum Subjects { ENGLISH = "english", MATH = "math", JAPANESE = "japanese" }
Enumで定数を利用する場合は、「Enum名.定数名」を記述します。その際、定義されていない定数を指定すると、当然エラーになります。図7は、定義されていないSCIENCEを指定した場合のエラー画面です。
一方、Enumは、[ ]内に定数値を指定することで、その定数名を取得できます。例えば、最初に定義したリスト20の(1)のSubjectsに対して、(2)のコードを記述すると、変数englishNameには、「ENGLISH」が格納されます。いわば、逆引きです。
enum Subjects { // (1) ENGLISH, MATH, JAPANESE } const englishName = Subjects[0]; // (2)
同様に、Subjects[1]だと「MATH」、Subjects[2]だと「JAPANESE」が格納されます。
では、リスト21のコードだとどうなるでしょうか。
const something = Subjects[5];
定数値5はSubjectsには存在しないので、本来、これはエラーになるべきですが、TypeScriptではエラーになりません。代わりにsomethingには、undefinedが格納されます。
この定義されていない定数が呼び出されてしまうというのは、Enumの問題といえます。これを解決するには、enum宣言時にconstを付記します。例えば、リスト22の(1)のような定義コードです。
const enum Subjects { // (1) ENGLISH, MATH, JAPANESE } const englishName = Subjects[0]; // (2)
この場合、そもそも、逆引きができなくなり、(2)のコードで図8のようなエラーになります。
constを付記すると逆引きができなくなりますが、undefinedが格納される心配はなくなります。
先ほど説明したように、Enumの定数値は、自動的に0から始まる定数が定義されます。もちろん、任意の数値を割り当てることもできますが、この数値型Enumには問題があります。
例えば、各定数値が数値型であるSubjectsを定義して、そのSubjects型を第2引数とするaddScore()があるとします。そのaddScore()に対して、リスト23のような任意の数値(100)を渡してもエラーになりません。
addScore("太郎", 100, 89.4);
これを防ぐためには、文字列型Enumを利用します。例えば、Subjectsを、リスト19のように文字列定義(リスト24の《1》に再掲載)に変更し、(2)のように記述した場合、問題なく動作しますが、(3)だと図9のようにエラーになります。
const enum Subjects { // (1) ENGLISH = "english", MATH = "math", JAPANESE = "japanese" } addScore("太郎", Subjects.MATH, 89.4); // (2) addScore("太郎", 100, 89.4); // (3)
同じく、リスト25のように、文字列を直接指定しても図10のエラーになります。
addScore("太郎", "science", 89.4);
最後に、複数の型指定を行う方法を紹介します。ここで、先ほどのタプル(taro)にもう一度登場してもらいましょう。このタプルの各要素を1つずつ取り出して、変数elementに格納することを考えます。コードにすると、リスト26のようになります。
const taro: [string, number, number] = ["太郎", 172.5, 71.5]; let element = taro[0]; // (1) element = taro[1]; // (2) :
このコードは、このままでは図11のようにエラーになります。
この原因は変数elementのデータ型にあります。(1)でtaroのインデックス0の要素を取り出してelementに代入しています。しかし、インデックス0はstring型です。この時点で、型推論により、elementはstring型変数となります。
一方、(2)のtaro[1]、すなわち、taroのインデックス1の要素は、number型です。となると、string型の変数elementにnumber型の値を代入しようとしていることになり、連載第1回で紹介したように、型の不一致により、当然エラーになります。
本来、型安全を考えると、1つの変数は、1つのデータ型であるべきです。しかし、場合によっては、このように複数のデータ型に対応しなければならないこともあり得ます。
このような場合に便利な方法が、複数のデータ型で変数を用意することです。これを、ユニオン型(共用型)といい、次のような構文を使います。
[構文5]ユニオン型での変数宣言 const 変数名: データ型|データ型|……
リスト26の変数elementを、ユニオン型を使って、stringとnumberの両方に対応させるには、リスト27のようなコードを記述します。
let element: number|string = taro[0]; element = taro[1]; :
こうすることで、リスト26(2)で起こったエラーがなくなります。
TypeScriptのTypeに注目し、あれこれ紹介する本連載の第2回はいかがでしたか? 今回は、JavaScriptにはない、TypeScript独特のデータ型の扱いとして、配列のデータ型宣言、タプル、Enum、ユニオン型を紹介しました。
次回は、TypeScriptでの関数の型にまつわるあれこれを紹介します。
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.