「型」から学ぶTypeScript、JavaScriptとは何が違うのか:TypeScriptのTypeあれこれシリーズ(1)
altJS、すなわち、JavaScriptの代わりとなる言語の筆頭である「TypeScript」。TypeScriptという言語名が示す通り、JavaScriptに「Type」、つまり、型の概念を持ち込んだものです。本連載では、このTypeScriptの型に関して、さまざまな方向から紹介していきます。連載1回目の今回は基本中の基本に当たる、型指定のあれこれを紹介します。
TypeScriptを取り上げる本連載は、TypeScriptのいわゆる言語入門ではありません。本連載の目的は、TypeScriptのType(型)に注目しつつ、その内容を掘り下げていくことです。その辺りを簡単に紹介してから、本編に入ります。
本連載で取り扱うテーマは「型」
TypeScriptは、JavaScriptの代わりとなる言語の一つとして開発された言語です。このような代替となる言語を「altJS」(alternative JavaScrip:代替JavaScript言語)といいます。TypeScriptの最大の特徴は、その言語名が示す通り、「Type」、すなわち「型」です。
JavaScriptにデータ型の概念を持ち込み、データ型を意識しなければコーディングできないようにしたのがTypeScriptです。意識しなければならないことが増えた分、一見、面倒に思えますが、実は逆なのです。型を意識することで、バグを未然に防ぐことができるのです。どのように便利なのかについては、おいおい紹介していきます。
本連載では、このようなメリットを味わいつつ、TypeScriptの型についてさまざまな機能を紹介します。具体的には、次のようなテーマを取り上げる予定です。
・基本のデータ型
・タプル、Enum、ユニオンなどJavaScriptにはないデータ型
・関数における型の扱い
・型ガード
・クラス
・ジェネリクス
・型エイリアス
・オブジェクト型リテラル
・インタフェース
・型定義ファイル
・型操作
・ユーティリティー型
これらのテーマには、「Java」「C#」といった、いわゆる静的型付け言語での型と同じようなテーマのものもあれば、静的型付け言語にはない、TypeScript独特の柔軟な型機能も含まれています。型に不慣れな読者だけではなく、静的型付け言語に慣れた読者にも、楽しんでいただけるでしょう。
なお、本連載では型以外の演算子や制御構文といった、いわゆるTypeScript入門に関しては、扱いません。そのような入門に関しては、@ITの「TypeScriptで学ぶJavaScript入門」連載を参照してください。
Visual Studio Codeの強力なサポートあり
TypeScriptは、実行前にコンパイル作業を経るために、その段階でコード上の問題を発見できます。これが、JavaScriptに比べて一段階バグを減らしやすい理由です。
さらに、「Visual Studio Code」(VS Code)のように、コーディング段階からエディタ内部でコンパイルと同等のチェックを行うツールもあります。本連載でも、VS Codeを利用し、そのエラー画面を随時見ながら解説を進めていきます。
データ型と型指定
イントロダクションを終え、いよいよ本編に入っていきましょう。
プログラミングにおいて、変数の利用というのは基礎中の基礎です。TypeScriptでは、変数を利用する際、つまり、基礎中の基礎から型を意識する必要があります。そこから話を始めていきましょう。
変数のデータ型
JavaScriptで変数を利用する場合、キーワード「let」、または、「const」を使って、例えば、以下のような記述をします。
let someValue = 254;
この同じ変数someValueに、その後、以下のように別の値を代入しても問題なく動作します。
someValue = "こんにちは";
ところが、TypeScriptでは、リスト2は成り立たず、エラーとなります。図1はリスト1とリスト2を記述したVS Codeの画面であり、確かにエラーになっています。
ここに、TypeScriptのType、つまり型の問題が現れています。
JavaScriptでは意識する必要がありませんが、データ(リテラル)には、その型が確実に存在します。リスト1の「254」は数値型であり、リスト2の「こんにちは」は文字列型です。この違いが、そのまま変数にも当てはまり、リスト1で変数someValueを初期値254で宣言した時点で、someValueは数値型となります。その後、リスト2で文字列型の値(データ)を同じ変数someValueに代入しようとした場合、JavaScriptでは、自動的に文字列型変数に変わります。この柔軟さ故に、データ型を意識しなくてもコーディングできる言語がJavaScriptです(図2)。
一方、TypeScriptでは、一度数値型で宣言された変数は、その後、数値型のままであり、文字列型という違うデータ型の値は代入できないような仕組みになっています(図3)。
この仕組みのおかげで、変数を利用する際、常にデータ型を意識する必要があり、その結果、バグを減らすことができるようになります。
型指定と型推論
このように、TypeScriptでは、データ型を常に意識しながら変数を利用していくため、その変数宣言でも次の構文のように、データ型を記述するようになっています。
[構文]変数宣言 let 変数名: データ型
この構文に従うと、リスト1は、以下のようになります。
let someValue: number = 254;
「:」(コロン)の次に記述された「number」が数値型を表すデータ型記述です。どのようなデータ型があるかは後述します。
ただし、これはあくまで原則論であり、実際には、TypeScriptでも、リスト1の記述で問題ありません。というのは、代入する初期値から変数のデータ型を自動的に決める仕組みがあるからです。これを、型推論といいます。
なお、この型推論が正しく動作するのは、変数宣言と初期値の代入が同時に行われるコードの場合のみです。例えば、リスト4のように変数宣言と初期値の代入を別の文にした場合、型推論は働きません。
let someValue; someValue = 254;
この場合は、先の構文通り、データ型を記述して、リスト5のようなコードにする必要があります。
let someValue: number; someValue = 254;
変数を利用する場合、宣言と初期値の代入を同時に行うことが多いので、それに合わせて、多くの変数宣言コードでは型推論に任せてデータ型を省略しています。ただ、場合によっては、先に宣言だけ済ませておいて、後で値を代入するコードというのもあります。その場合にデータ型の記述を忘れないようにしてください。
データ型の種類
次に、TypeScriptのデータ型を見ていきましょう。
3種あるプリミティブ型
TypeScriptのデータ型には、「プリミティブ型」と「オブジェクト型」の2種類があります。この2種類の違いは、内部に処理、つまり、メソッドを含むことができるかどうかです。
プリミティブ型は内部にメソッドを含むことができず、ただ値を保持するだけのものです。プリミティブ型に含まれるのは、主に数値を扱うnumber型、文字列を扱うstring型、bool値(trueとfalse)を扱うboolean型の3種類があります。それぞれ、データ型を記述した変数宣言は、リスト6のようになります。
let someNum: number = 45.228; let someStr: string = "こんにちは"; let someBoo: boolean = true;
もちろん、この3変数とも、型推論が有効なため、実際にはリスト7のようにデータ型を記述せずに変数宣言するのが通常です。
let someNum = 45.228; let someStr = "こんにちは"; let someBoo = true;
【補足】文字列リテラル
TypeScriptの文字列リテラルは、JavaScriptと同様に、ダブルクオーテーション("")、シングルクオーテーション('')、バッククオーテーション(``)で囲む3種類があります。本連載で文字列リテラルを記述する場合、基本はダブルクオーテーションを利用し、式展開や複数行文字列を扱う場合にのみバッククオーテーションを利用することにします。これは、TypeScriptの公式ドキュメントに従った使い方であり、さらにJavaなどシングルクオーテーションが文字列を表さない言語があるからです。
オブジェクト型
ここまで紹介してきた3種類のプリミティブ型と後述する特殊なもの以外は、全てオブジェクト型といえます。例えば、JavaScriptには、ビルトインオブジェクトとしてさまざまなオブジェクトが用意されており、それらを利用する変数がオブジェクト型になります。ビルトドインオブジェクトの一つである「Date」を利用するリスト8を例に挙げます。
const now = new Date(); // (1) const nowStr = now.toDateString(); // (2)
型推論を利用しているので、変数nowにはデータ型が記述されていませんが、変数nowはDate型です。もしデータ型を記述するならば、リスト9のようになります。
const now: Date = new Date();
そして、このDate型は、まさにオブジェクト型の一つであり、内部にメソッドを持ちます。実際に、(2)でメソッドの一つである「toDateString()」を利用しています。このメソッドは、変数nowが表す日付の文字列を取得するメソッドです。ということは、戻り値を格納した変数nowStrはstring型となります。ここでも型推論を利用しているので、データ型を省略していますが、データ型を記述するならば、リスト10のようになります。
const nowStr: string = now.toDateString();
これを、違ったデータ型で宣言した変数、例えば、次のようにnumber型にすると、エラーになります。
const nowStr: number = now.toDateString();
図4はVS Codeでリスト11を記述してエラーになった画面です。
このようなエラー表示は、データ型を厳密に扱う言語ならではであり、さらに、そのエラー表示をサポートしたVS Codeなどのエディタとの組み合わせで威力を発揮します。
さらに、TypeScriptとVS Codeの組み合わせでは、入力補完も強力です。例えば、図5はnowに続けて「.」(ドット)を入力した際に、候補が表示された画面です。
この候補表示は、nowがDate型だとVS Codeが理解しているからこそ表示されるものです。
このように、TypeScriptの型を使いこなしていけばいくほど、コーディングを強力にサポートしてくれるようになり、その結果、さまざまなバグを減らすことができるようになります。
特殊なデータ型
最後に特殊なデータ型を幾つか紹介します。
any型とunknown型
データ型と型指定の節で、型を記述しないコードを紹介しました。
let someValue;
このように、変数の型が指定されておらず、しかも型推論も働かない場合、その変数は自動的にany型となります。any型は、その名称通り、どんなデータ型でもOKであり、いわば、データ型チェックを放棄した変数といえます。
このany型と似たもので、unknown型があります。unknownも、any同様に、どんなデータ型でもOKというデータ型ですが、2つの型には違いがあります。any型の変数は、そのプロパティやメソッドへアクセスできます。unknown型の変数はアクセスを許しません。例えば、リスト13は成立します。
let now1: any = new Date(); // (1) const nowStr1 = now1.toDateString(); // (2) now1 = 556; // (3) nowStr1 = now1.toDateString(); // (4)
(1)で宣言した変数now1は、本来なら、型推論を利用してDate型とすべきですが、これをあえてany型で宣言しました。そのnow1に対して(2)でメソッドtoDateString()を実行しています。これは問題なく動作します。しかし、any型で宣言してしまっているために、(3)のように数値型の値である556の代入も、(4)のようにその数値型変数には本来ないはずのメソッドtoDateString()の呼び出しコードも、コンパイル時にはエラーとなりません。もちろん、このコードは、実行時にエラーになります。
一方、似たようなコードとしてリスト14を記述したとします。
let now2: unknown = new Date(); // (1) let nowStr2 = now2.toDateString(); // (2)
この場合、(2)のコードを記述した段階で図6のようにエラーとなります。
本来、存在するメソッドへのアクセスすら許さないのがunknownです。
このように、any型やunknown型を利用することで、データ型を指定せずに変数を利用することも、TypeScriptでは可能です。ただし、ここで見たように、any型というのは便利なようで非常に危険であり、TypeScriptの型の利点が全く生かされないのです。any型での変数宣言は極力避けるようにしましょう。
それでも、先述のように、型推論が働かないといった理由で、知らないところで変数がany型として扱われてしまう場合があります。それを避けたい場合は、「noImplicitAny」オプションを有効にしてコンパイルを行うと、そのような変数を知らせてくれます。
一方、unknownは、上で確認したように、anyに比べて比較的安全です。そのため、変数宣言時点でデータ型を決定できない変数に関しては、unknown型で宣言しておき、データ型が確定された段階で、そのデータ型に型変換を行う、という使い方が考えられます。具体的な方法は、本連載中で扱うテーマの一つである型ガードで紹介します。
undefined型とnull型
undefinedとnullも特殊なデータ型です。JavaScriptでは、両方とも値がないものを表すキーワードとなっており、あまり違いを意識しなくてもコーディングできました。TypeScriptでも、同様に、意識しなくてもコーディングできます。しかし、確実に違います。例えば、リスト15を見てください。
let un = undefined; // (1) let nu = null; // (2) console.log(un == nu); // (3) console.log(un === nu); // (4) un = 551; // (5) nu = new Date(); // (5) un = null; // (6) nu = undefined; // (6)
(3)と(4)の実行結果は、それぞれtrueとfalseです。つまり、両方とも値がない状態を表しますが、データ型が違います。
厳密にいえば、undefinedという値は、プリミティブ型の一種であるundefined型です。一方、nullという値は、オブジェクト型の一種であるnull型です。両方とも型が違うので、(4)ではfalseとなります。
では、(5)や(6)のコードはどうでしょうか。(5)ではundefined型の変数unにnumber型の値551を、null型の変数nuにDate型の値を、それぞれ代入しています。さらに、(6)ではunにnullを、nuにundefinedを代入しています。型の厳密性をうたうならば、このコードは成立しないはずですが、いずれも問題なく動作します。
種明かしは、(1)と(2)にあります。実は、初期値がundefinedやnullでは、型推論が働かず、その変数はany型になります(図7)。
この状態を避ける方法は、リスト15の(1)と(2)を、リスト16のようにデータ型を明示して記述します。
let un: undefined = undefined; let nu: null = null; :
このコードを記述した途端、リスト15の(5)はエラーになります(図8)。
ところが、このように型を指定しても、リスト15の(6)はエラーになりません。undefinedとnullの型指定で、undefinedとnumber、nullとDateの型の違いを認識するようになったのですが、依然、undefinedとnullの違いは認識していません。
この違いを認識させるためのコンパイルオプションが、TypeScriptにはあります。「strictNullChecks」です。このオプションを指定してリスト16をコンパイルすると、次のようなエラーが表示されます。
sample05.ts:11:1 - error TS2322: Type 'null' is not assignable to type 'undefined'. 11 un = null; ~~ sample05.ts:12:1 - error TS2322: Type 'undefined' is not assignable to type 'null'. 12 nu = undefined;
このように、nullとundefinedを厳密に区別したコードを記述したい場合に、strictNullChecksオプションは便利です。
なお、通常、値がないことを意図的に指定したい場合は、次のように、undefinedではなく、nullを使います。
let nameList = null;
リスト17はこの後、別の処理でnameListに値が代入されることを想定しています。
symbol型
symbol型は、ECMAScript 2015(ES2015)からJavaScriptに導入されたデータ型で、コード全体で唯一となるような値を生成します。例えば、以下のコードを見てください。
const name = "田中太郎"; // (1) const tanaka = name; // (2) const taro = name; // (3) console.log(tanaka === taro); // (4) const tanakaS = Symbol(); // (5) const taroS = Symbol(); // (6) console.log(tanakaS === taroS); // (7)
(1)で文字列変数nameを、初期値「田中太郎」で生成し、そのnameと同じ値を持つ変数tanakaを(2)で、taroを(3)で生成しています。このtanakaとtaroは、値もデータ型も同じであるため、(4)での比較は、当然trueとなります。
一方、(5)と(6)では、symbol型変数tanakaSとtaroSを用意しています。symbol型変数を生成するには、Symbol()というコードを記述します。右辺が同じコードなので、tanakaSとtaroSは、値もデータ型も同じように見えますが、「Symbol()」というコードそのものがユニーク(一意)となるように値が生成されるため、(7)の比較は常にfalseです。これは、==演算でも同様です。実際、VS Codeでは、(7)を記述した時点で、図9のようにエラー表示されます。
symbol型変数は、一意が確保できることから、オブジェクトにデータを登録する際のキーとして利用されたり、定数の値として利用されたりします。なお、Symbol()を含むコードをコンパイルする際は、targetオプションとして「es2015」を指定する必要があります。
bigint型
bigint型は、ECMAScript 2020(ES2020)からJavaScriptに導入されたデータ型です。number型では桁数が足りない数値演算を行いたい場合に利用します。データ型を明示する変数宣言の場合、bigintを記述しますが、次のようなコードはエラーになります。
const num: bigint = 551;
なぜなら、単に551と記述した場合、number型になるからです。そこで、次のように記述します。
const num = 551n;
ポイントは、「n」です。これでbigint型になります。あるいは、次のように、BigInt()コードを記述しても構いません。
const num = BigInt(551);
どちらの場合も、型推論が利用できるので、データ型の記述は省略できます。
なお、bigint型を含むコードをコンパイルする際は、targetオプションとして「es2020」を指定する必要があります。
まとめ
TypeScriptのTypeに注目し、あれこれ紹介する本連載の第1回はいかがでしたか? データ型を意識することで、バグを軽減でき、さまざまな恩恵にあずかれます。次回は、JavaScriptにはない、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.