JavaScriptにはないTypeScript独自の型あれこれTypeScriptのTypeあれこれシリーズ(2)

altJS、すなわち、JavaScriptの代わりとなる言語の筆頭である「TypeScript」。TypeScriptという言語名が示す通り、JavaScriptに「Type」、つまり、型の概念を持ち込んだものです。本連載では、このTypeScriptの型に関して、さまざまな方向から紹介していきます。連載1回目の前回は、基本中の基本に当たる、型指定にまつわるあれこれを紹介しました。今回はJavaScriptにはない、TypeScript独自の型のあれこれを紹介します。

» 2021年11月22日 05時00分 公開

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

「TypeScriptのTypeあれこれシリーズ」のインデックス

連載:TypeScriptのTypeあれこれシリーズ

配列の要素と型

 配列はTypeScriptオリジナルなものではなく、JavaScript由来のものです。配列のTypeScriptらしい使い方から紹介していきましょう。

JavaScriptの配列の問題点

 JavaScriptで配列変数を用意する場合、リスト1のようなコードを記述します。

const array = [];
リスト1

 このarrayに対して、リスト2のように各要素を格納するコードを記述したとします。

array[0] = 35;  // (1)
array[1] = "こんにちは";  // (2)
array[2] = new Date();  // (3)
リスト2

 (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;
});
リスト3

 ただし、あくまでも配列が数値で構成されている場合にしか使えません。リスト2のarrayのように、数値以外のものを要素とする配列の場合は、当然、合計値になりません。しかも、JavaScriptでは、このようなコードを簡単に防ぐ方法がありません。

TypeScriptの配列

 一方、TypeScriptには、データ型という仕組みがあるので、配列に対しても、データ型を指定することで、このような問題を防ぐことができます。TypeScriptの配列構文は次の通りです。

[構文1]配列宣言
const 配列変数名: 各要素のデータ型[]

 リスト2のarrayを数値型に限定するならば、リスト4のようなコードになります。

const array: number[] = [];
array[0] = 35;
リスト4

 これで、配列変数arrayの各要素には数値以外代入できなくなります。もし、「こんにちは」を代入しようとすると、図1のようなエラーになります(以下、「Visual Studio Code」を利用)。

図1 数値型配列に文字列を代入しようとしてエラーになった画面

 TypeScriptの配列では、型指定のおかげで各要素の型が同一になります。同種のデータをまとめておくもの、という配列本来の働きを実現できるのです。

配列宣言には別の形がある

 配列を宣言する構文は、構文1とは異なるものがあります。

[構文2]配列宣言
const 配列変数名: Array<各要素のデータ型>

 リスト4を構文2で書き換えると、リスト5になります。

const array: Array<number> = [];
array[0] = 35;
リスト5

 通常、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)
リスト6

 (1)では既存の要素の変更しており、(2)では新しい要素を追加しています。このどちらも可能です。一方、const宣言配列への再代入とは、リスト7のようなコードを指します。

array1 = [2, 4, 6, 8, 10];
リスト7

 この場合は、図2のようなエラーになります。

図2 const宣言配列へ再代入しようとしてエラーとなった画面

 つまり、const宣言配列は、再代入不可ではありますが、不変(イミュータブル)ではないのです。もし、不変の配列を作成するなら、「readonly」を利用します。例えば、リスト8のようなコードです。

const array2: readonly number[] = [1, 3, 5, 7, 9];
リスト8

 このarray2の各要素を変更しようとすると、図3のエラーになります。

図3 readonlyが付いた配列の要素を変更しようとしてエラーとなった画面

配列とタプルの違いは

 同種のデータをまとめておくものが配列なら、異種のデータをまとめておく方法にはどのようなものがあるのでしょうか。JavaScriptには古くからオブジェクトというものがあります。モダンなJavaScriptでは、クラスの利用も考えられます。オブジェクトもクラスも当然、TypeScriptで利用できます。ここでは、TypeScriptならではの方法として、もっと簡易に異種データをまとめることができる「タプル」を紹介します。

タプルとは

 タプルは要素数と各要素の型を限定した配列といえます。具体例を見てみましょう。

 例えば、ある人の名前と身長、体重をまとめて1つの変数にしたい場合、オブジェクトやクラスを利用する方法もありますが、タプルならリスト9のようにもっと簡単に、使い捨て感覚で利用できます。

const taro: [string, number, number] = ["太郎", 172.5, 71.5];
リスト9

 右辺のリテラル部分を見ると、型違いのデータが格納された配列のように見えますが、左辺の変数宣言部分のデータ型を記述する部分では、「[ ]」の中に3個のデータ型があります。この記述により、右辺に記述できるリテラルでは、要素が3個と決まり、さらに、インデックス0には文字列、インデックス1と2には数値しか代入できなくなります。

 これがタプルです。構文としてまとめると次の通りです。

[構文3]タプル宣言
const タプル変数名: [データ型, データ型, ……]

 タプルは要素数と各要素のデータ型が決まってしまうため、それ以外の代入が許されなくなります。例えば、リスト9で、インデックス3、つまり4個目に代入しようとすると、図4のようなエラーになります。

図4 タプルに要素数以上に値を代入しようとしてエラーになった画面

 さらに各要素の型もおのおの決まっているので、それに反した値を代入しようとしてもエラーになります。図5は数値指定の要素であるインデックス2、つまり3個目に文字列を代入しようとしてエラーとなった画面です。

図5 タプルの数値要素に文字列を代入しようとしてエラーになった画面

タプルも不変にできる

 タプルも配列同様に、constで宣言していても、不変ではありません。例えば、リスト9の後、taroのインデックス2に対して、数値型であればリスト10のように要素の値を書き換えることができます。

taro[2] = 70.8;
リスト10

 もちろん、リスト11のような値全体の再代入はエラーになります。

taro = ["たろう", 165.5, 68.4];
リスト11

 一方、タプルでも配列同様に、readonlyを付記することで不変なものが作成できます。例えば、リスト12のようなコードです。

const jiro: readonly [string, number, number] = ["次郎", 165.5, 68.4];
リスト12

 このjiroのインデックス2に対して、先のtaroと同じようなコードを記述すると、図6のようにエラーになります。

図6 readonlyが付いたタプルの要素を変更しようとしてエラーとなった画面

タプルとEnumは何が違うのか

 タプルは配列の応用というイメージでした。一方、次に紹介する「Enum」は、JavaScriptにはなく、TypeScript独自のデータ構造です(もちろん、JavaScriptと違う系列の言語、例えば、JavaなどにはEnumが存在します)。

Enumとは

 Enumは列挙型といわれ、いわば定数をまとめておくためのものです。例えば、英語、数学、国語の成績を管理するアプリケーションを作成するとして、その中で、各教科を区別するために、リスト13のような定数を定義したとします。

const ENGLISH = 0;
const MATH = 1;
const JAPANESE = 2;
リスト13

 アプリケーション中に各教科の点数を登録する関数として、リスト14の(1)のような「addScore()」があるとしたなら、これらの定数を利用して、(2)のようなコードを記述することも可能です。

function addScore(name: string, subject: number, score: number) {  // (1)
	:
}
addScore("太郎", MATH, 89.4);  // (2)
リスト14

 このような定数をまとめておく仕組みとしてEnumを利用すると、コードがスッキリします。Enumの構文は次の通りです。

[構文4]Enum宣言
enum 名前 {
	定数名,
	定数名,
	:
}

 各教科を区別する定数をEnumにするとリスト15の(1)のコードになります。

enum Subjects {  // (1)
	ENGLISH,
	MATH,
	JAPANESE
}
function addScore(name: string, subject: Subjects, score: number) {  // (2)
	:
}
リスト15

 このEnumを利用する場合、addScore()の定数を指定する第2引数のデータ型として、Enum型を指定でき、呼び出すコードはリスト16のように記述できます。

addScore("太郎", Subjects.MATH, 89.4);
リスト16

Enumの定数値の扱い

 先の定数宣言とは違い、Enumでは数値を指定する必要がありません。記述順に自動的に0から始まる連番が付与されます。リスト15のSubjectsだと、Subjects.ENGLISHが0、Subjects.MATHが1、Subjects.JAPANESEが2になります。

 この番号を明示的に指定することもできます。例えば、リスト17のように、最初の定数の値だけを指定すると、それ以降は、その次の値を定義してくれます。

enum Subjects {
	ENGLISH = 1,
	MATH,
	JAPANESE
}
リスト17

 この場合、Subjects.ENGLISHが指定された1、Subjects.MATHが2、Subjects.JAPANESEが3になります。

 また、リスト18のように、全ての値を指定することもできます。

enum Subjects {
	ENGLISH = 10,
	MATH = 20,
	JAPANESE = 30
}
リスト18

 定数値として、リスト19のように文字列を割り当てることもできます。

enum Subjects {
	ENGLISH = "english",
	MATH = "math",
	JAPANESE = "japanese"
}
リスト19

Enumは「逆引き」できる

 Enumで定数を利用する場合は、「Enum名.定数名」を記述します。その際、定義されていない定数を指定すると、当然エラーになります。図7は、定義されていないSCIENCEを指定した場合のエラー画面です。

図7 定義されていないEnum定数を指定してエラーになった画面

 一方、Enumは、[ ]内に定数値を指定することで、その定数名を取得できます。例えば、最初に定義したリスト20の(1)のSubjectsに対して、(2)のコードを記述すると、変数englishNameには、「ENGLISH」が格納されます。いわば、逆引きです。

enum Subjects {  // (1)
	ENGLISH,
	MATH,
	JAPANESE
}
const englishName = Subjects[0];  // (2)
リスト20

 同様に、Subjects[1]だと「MATH」、Subjects[2]だと「JAPANESE」が格納されます。

Enumには問題がある

 では、リスト21のコードだとどうなるでしょうか。

const something = Subjects[5];
リスト21

 定数値5はSubjectsには存在しないので、本来、これはエラーになるべきですが、TypeScriptではエラーになりません。代わりにsomethingには、undefinedが格納されます。

 この定義されていない定数が呼び出されてしまうというのは、Enumの問題といえます。これを解決するには、enum宣言時にconstを付記します。例えば、リスト22の(1)のような定義コードです。

const enum Subjects {  // (1)
	ENGLISH,
	MATH,
	JAPANESE
}
const englishName = Subjects[0];  // (2)
リスト22

 この場合、そもそも、逆引きができなくなり、(2)のコードで図8のようなエラーになります。

図8 定義されていないEnum定数を指定してエラーになった画面

 constを付記すると逆引きができなくなりますが、undefinedが格納される心配はなくなります。

数値型Enumには問題がある

 先ほど説明したように、Enumの定数値は、自動的に0から始まる定数が定義されます。もちろん、任意の数値を割り当てることもできますが、この数値型Enumには問題があります。

 例えば、各定数値が数値型であるSubjectsを定義して、そのSubjects型を第2引数とするaddScore()があるとします。そのaddScore()に対して、リスト23のような任意の数値(100)を渡してもエラーになりません。

addScore("太郎", 100, 89.4);
リスト23

 これを防ぐためには、文字列型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)
リスト24
図9 文字列Enumを指定するところで数値を記述してエラーになった画面

 同じく、リスト25のように、文字列を直接指定しても図10のエラーになります。

addScore("太郎", "science", 89.4);
リスト25
図10 文字列Enumを指定するところで直接文字列を記述してエラーになった画面

複数のデータ型に対応するユニオン型

 最後に、複数の型指定を行う方法を紹介します。ここで、先ほどのタプル(taro)にもう一度登場してもらいましょう。このタプルの各要素を1つずつ取り出して、変数elementに格納することを考えます。コードにすると、リスト26のようになります。

const taro: [string, number, number] = ["太郎", 172.5, 71.5];
let element = taro[0];  // (1)
element = taro[1];  // (2)
	:
リスト26

 このコードは、このままでは図11のようにエラーになります。

図11 リスト26の(2)でエラーになった画面

 この原因は変数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];
	:
リスト27

 こうすることで、リスト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.

スポンサーからのお知らせPR

注目のテーマ

Microsoft & Windows最前線2025
AI for エンジニアリング
ローコード/ノーコード セントラル by @IT - ITエンジニアがビジネスの中心で活躍する組織へ
Cloud Native Central by @IT - スケーラブルな能力を組織に
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。