TypeScriptの型エイリアスにまつわるあれこれ:TypeScriptのTypeあれこれシリーズ(9)
altJS、すなわち、JavaScriptの代わりとなる言語の筆頭であるTypeScript。そのTypeScriptは、言語名が示す通り、JavaScriptにType、つまり、型の概念を持ち込んだものです。本連載では、このTypeScriptのType(型)に関して、さまざまな方向から紹介していきます。前回は、クラスや関数を利用する段階で型の指定ができるジェネリクスについて、紹介しました。今回は、今あるデータ型をもとに、新たなデータ型を定義できる型エイリアスについて、あれこれ紹介していきます。
型エイリアスとtypeキーワード
まず、これまでに学んできた型について復習しておきます。
第1回で紹介したように、TypeScriptのデータ型には、小文字から始まるプリミティブ型と大文字から始まるオブジェクト型があります。
プリミティブ型には、number、string、boolean、symbol、bigintなどJavaScriptでおなじみのものに加えて、特殊なデータ型として、any、unknown、undefined、null、void、neverなどが含まれます。一方、オブジェクト型は、DateやMapなどのJavaScriptの組み込みオブジェクトをはじめとして、クラスを自作することで無数に定義できます。
また、第2回では、複数のデータ型をまとめて扱えるユニオン型を紹介しました。例えば、第2回のリスト27では、次のコードを紹介しています。このうち、「number|string」がユニオン型を表すコードです。
let element: number|string = taro[0]; element = taro[1];
型を新たに定義するtypeキーワード
TypeScriptには、この「number|string」というデータ型に名前を付けて、一つの新しいデータ型とする仕組みがあります。これが型エイリアス(Type Alias)です。
型エイリアスを宣言するには、typeキーワードを使用します。例えば、リスト1の「number|string」を一つのデータ型として名前を付けたのが、リスト2の(1)です。
type NumStr = number|string; // (1) : let element: NumStr = taro[0]; // (2) element = false; // (3)
リスト2の(1)では、「number|string」をNumStrというデータ型としています。このように、新たなデータ型を定義したならば、それ以降は(2)のように、そのデータ型の名前を記述するだけで利用できるようになります。
ここで、typeキーワードの使い方を構文としてまとめておきます。
[構文]型エイリアス type 型名 = 型記述;
typeのデータ型はエイリアス
ただし、typeキーワードで、それまでになかったデータ型を作ることができるわけではありません。例えば、リスト2の(1)で定義したNumStrは、あくまでも「number|string」に名前を付けて、再利用しやすいようにしただけです。ということは、NumStrの実体は「number|string」というユニオン型であり、もともと存在したデータ型の組み合わせに過ぎません。よってリスト2の(3)のコードは「number|string」の型指定に反するため、図1のエラーとなります。
typeキーワードによって定義されたデータ型を「型エイリアス」と呼ぶのは、その実体がこれまでの型指定そのものだからです。
type利用のバリエーション
typeキーワードの使い方、つまり型エイリアスの基本を理解したところで、ここからさまざまな使い方を紹介していきます。
既存の型の別名
typeキーワードは、前節で紹介したような既存の型の組み合わせに名前を付けるだけではなく、一つの既存の型に別名を付ける使い方も可能です(リスト3)。
type OriginalStr = string; // (1) function showName(name: OriginalStr): void { // (2) console.log(name); } showName("田中太郎"); // (3) showName(35); // (4)
リスト3の(1)は、単なるstring型に対してOriginalStrという別名を付けているコードです。このOriginalStrを引数のデータ型として指定しているのが、(2)のshowName()関数です。引数がOriginalStr型とはいえ、その実体はstringなので、(3)のように文字列はそのまま引数として渡せますが、(4)のように数値を渡すと図2のエラーとなります。
ここで、エラーメッセージに注目してください。「引数の型string」という表記になっており、OriginalStrがあくまで別名にすぎないことが、これからも分かります。このことから、単独のデータ型に対して別名を指定する方法は、原理的には可能ですが、あまり意味がないといえます。
関数のデータ型エイリアス
第3回では、関数そのもののデータ型を紹介しました。例えば、第3回のリスト21の関数シグネチャを再掲すると、リスト4のようになります。
function calc2Rand(func: (rand1: number, rand2: number) => number): number { : }
この関数calc2Rand()の引数funcはコールバック関数です。そのデータ型だけを抜き出したものがリスト5です。
(rand1: number, rand2: number) => number
typeキーワードでは、この関数のデータ型も型エイリアスとして定義できます(リスト6)。
type Calc2RandFunc = (rand1: number, rand2: number) => number; // (1) function calc2Rand(func: Calc2RandFunc): number { // (2) : }
リスト6の(1)が、リスト5の型記述をCalc2RandFunc型として定義しているコードです。このように関数の型そのものを一つのデータ型として定義でき、一度定義してしまえば、(2)のようにコールバック関数のデータ型指定として利用できます。
タプルの型定義
もう一度、第2回で紹介した内容に登場してもらいましょう。それは、タプルです。第2回では、ある人の名前と身長、体重をまとめて一つの変数とするためのタプルとして、リスト7のようなコードを紹介しています。このコードは、第2回リスト9の再掲です。
const taro: [string, number, number] = ["太郎", 172.5, 71.5];
typeキーワードでは、このようなタプル型も型エイリアスとして定義できます(リスト8)。
type BMIData = [string, number, number]; // (1) const taro: BMIData = ["太郎", 172.5, 71.5]; // (2) const jiro: BMIData = ["次郎", "イケメン", "頭脳明晰"]; // (3)
リスト8の(1)が、リスト7のタプルの型記述をそのままBMIData型とし定義しているコードで、このBMIData型を型指定として変数を用意しているのが(2)です。(2)の右辺を見ても分かるように、型定義通りのタプルを記述する必要があります。これを、(3)のようにインデックス1と2に文字列を指定すると、図3のようにエラーとなります。
リテラル型
TypeScriptの型記述には、リテラルをそのままデータ型として定義できる仕組みがあります。これは、先に具体例を見ていただきましょう。
type HttpMethod = "GET"|"POST"|"PUT"|"DELETE"; // (1) function doAccess(method: HttpMethod) { // (2) : }
(1)で型エイリアスとして、HttpMethodを定義しています。HttpMethodは「|」が使用されたコードなので、ユニオン型です。ただし、いわゆるデータ型を組み合わせたユニオン型ではなく、それぞれの記述はGETやPOSTといった文字列そのものです。このように、TypeScriptではリテラルをデータ型のように定義することが可能であり、ここで定義したHttpMethod型の変数は、(1)で定義したGET、POST、PUT、DELETEのいずれかの文字列である必要があります。それ以外は許されません。
例えば、(2)では関数doAccess()の引数として、このHttpMethod型のmethodを定義しています。よって、この引数methodには、リスト10の(1)のように上記4個の文字列以外を渡せなくなります。
const nowMethod = "GET"; // (1) doAccess(nowMethod); // (1) const thereMethod = "TRY"; // (2) doAccess(thereMethod); // (2)
これを、(2)のように定義されていない文字列を渡そうとすると、図4のようにエラーとなります。
これは、単なる文字列、つまりstring型として指定した引数では実現できない仕組みです。
neverの活用法の補足
typeキーワードに慣れてきたところで、本節の最後に、第4回の最後に紹介した「neverの活用法」について補足しておきます。以下に、第4回のリスト17を再掲しておきます。
function addElement(element: number, list: number[]|Set<number>|Map<number, number>): void
この関数addElement()は、もともと第4回のリスト7からの流れを受けており、その段階では、第2引数listのデータ型は「number[]|Set<number>」でした。この時点でlistのデータ型を、リスト12のようにtypeキーワードを使って型定義したとします。
type Collections = number[] | Set<number>;
すると、addElement()関数はリスト13のような記述になり、listの型の実体がリスト11に比べて、すぐには分からないようになります。
function addElement(element: number, list: Collections): void { : }
この状態で、Collections型にリスト14のようにMapを追加したとすると、addElement()関数内の変更を忘れる可能性が大いにあります。
type Collections = number[] | Set<number>|Map<number, number>;
このような場合でも、関数内の変更を忘れないようにコンパイルエラーの形で教えてくれるのが、第4回で紹介したneverの役割だったわけです。
オブジェクト型リテラル
JavaScriptには、古くからあるデータ表現形式としてオブジェクトリテラルがあります。今回最後の話題として、このオブジェクトリテラルの欠点を確認し、TypeScriptではどのようにその欠点が克服されているかということを紹介します。そこで、typeがまた活躍します。
オブジェクトリテラルの問題点
例えば、名前と身長と体重を管理するデータ形式を考えます。リスト7やリスト8では、タプルを利用してこのデータを実現していますが、JavaScriptではリスト15のようにオブジェクトリテラルを利用することが多いでしょう。
const taro = { name: "太郎", height: 172.5, weight: 71.5 }
このJavaScriptのオブジェクトリテラルの問題点は、プロパティを自由に増減できてしまうことです。本来、名前と身長と体重を過不足なく格納したデータとしておきたいのに、リスト16のように余分なプロパティとしてageを追加しても、コード上は問題になりません。
const taro = { name: "太郎", height: 172.5, weight: 71.5, age: 38 }
同様に、リスト17のように体重を表すweightを記述し忘れても、問題になりません。
const taro = { name: "太郎", height: 172.5 }
もちろん、このtaroを利用したコードによっては、動作エラーとなることもあり得ます。しかし、このままどこかに保存するようなプログラムの場合は、データ欠損の状態で保存されてしまいます。データの保全上、のちのち大問題となる可能性もあります。
オブジェクトリテラルの型定義
TypeScriptでは、このオブジェクトリテラルへ事前に型を定義できるため、問題が起きにくくなっています(リスト18)。
type BMIData = { // (1) name: string; // (2) height: number; // (3) weight: number; // (4) }; const taro: BMIData = { // (5) : }
リスト18の(1)がオブジェクトの型を定義している部分であり、ここでtypeキーワードが活躍します。(1)では型名をBMIDataとしており、右辺に実際のオブジェクトの型定義を記述しています。このオブジェクトの型定義を構文としてまとめたものが以下です。
[構文]オブジェクトの型定義 type 型名 = { プロパティ名: データ型; : }
このオブジェクトの型定義は、一見してオブジェクトリテラルと似ています。違うのは、「プロパティ名のコロンに続いてデータ型を記述し、末尾をセミコロンとすること」です。リスト18の(2)ではstring型のnameプロパティを、(3)ではnumber型のheightを、(4)ではnumber型のweightを、それぞれ定義しています。
このようにオブジェクトの型を一度定義しておくと、リスト18の(5)のようにオブジェクトリテラルを定義する際に、その型としてオブジェクトの型定義を指定できます。このように型定義されたオブジェクトリテラルを、「オブジェクト型リテラル」といいます。
オブジェクト型リテラルは定義通り
このオブジェクト型リテラルの最大の利点は、各プロパティが定義通りでないとエラーとなる点です。例えば、リスト16のようにageプロパティを勝手に記述すると、図5のエラーとなります。
リスト17のようにweightを記述し忘れても、同様に図6のエラーとなります。
もちろん、プロパティのデータ型が違っていてもエラーとなります。図7は、number型でなければならないweightに文字列を記述した場合の結果です。
省略可能なプロパティ定義
オブジェクトの型を定義する際、省略可能なプロパティを定義したい場合は、リスト19のageプロパティのように「?」を記述します。
type BMIData = { name: string; height: number; weight: number; age?: number; };
この場合は、ageプロパティをオブジェクトリテラルで記述してもしなくても構いません。すなわちリスト15のようにageを記述しない場合と、リスト16のようにageを記述した場合の双方において、エラーとなりません。
まとめ
TypeScriptのTypeに注目し、あれこれ紹介する本連載の第9回はいかがでしたか?
今回は、今あるデータ型を組み合わせて新たなデータ型を定義できる型エイリアスを紹介しました。その中心となるのが、typeキーワードでした。また、このtypeキーワードを利用したオブジェクト型リテラルについても紹介しました。実は、オブジェクト型リテラルは、typeキーワード以外に、インタフェースを利用しても定義できます。
次回は、このインタフェースを紹介する予定です。
筆者紹介
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.