TypeScriptの基礎と文法
TypeScript早わかりチートシート
今がTypeScriptを学び始めるベストタイミング! TypeScriptを使うときに役立つ情報がまとまったチートシート(1クリックで試せるサンプル付き)。
4月3日にTypeScript 1.0がリリースされました。1.0はTypeScriptの初めての安定版リリースで、現在使えるTypeScriptの構文は、この後のアップデートで変更されたり使えなくなったりすることがありません。つまり、今がTypeScriptを学び始めるちょうどいいタイミングである!ということです。
そこで、この記事ではTypeScriptを使うときに役立つ情報をチートシートとしてまとめます。サンプルプログラムが必要な箇所には「TypeScript Playground」を利用し、1クリックで試すことができるリンクを示します。
TypeScriptの位置付け
まずは、TypeScriptがJavaScriptに対してどういう位置付けにあるかを解説します。TypeScriptの位置付けを知ることで、JavaScriptの知識をどう活用できるか、どう参考にできるかが分かります。
JavaScriptの仕様はECMAScript 5.1としてまとめられています。TypeScriptは、ECMAScript 5.1の仕様を全て受け継ぎつつ、静的型付け言語に変身するための仕様と、現在策定行中のECMAScript 6をベースにしたクラス定義ができるように拡張された言語です。
つまり、JavaScriptの仕様の良いところも、悪いところも、微妙なところも全て受け継いでいます。
Let's play TypeScript!
変数の型注釈と型推論
var str1: string = "string1";
var str2: number = "string2"; // エラー!
var str3 = "string3"; // 初期化子の型から型推論されstring型を指定したのと等価
str3 = 1; // エラー!
var b: boolean = true;
var n: number = 1;
var a: any = true;
a = 1; // any は何でもOK!
|
TypeScriptには変数や関数などに型が存在していて、明示的にどの型を使うかを指定できます(型注釈)。値からの型推論も行えるため、変数の定義と代入を同時に行うようにすると、JavaScriptと変わらぬ記述性と堅固な型チェックの恩恵を受けることができます。
また、string、number、booleanの3種類は、プリミティブな型として最初から利用できます。
クラスを利用する
class ClassName extends ParentClassName implements InterfaceName {
static classVariable: ClassVariableType;
instanceVariable: InstanceVariableType;
constructor(arg: ArgType) {
}
static method(arg: ArgType): ReturnType {
return returnValue;
}
method(arg: ArgType): ReturnType {
return returnValue;
}
}
|
TypeScriptではクラス変数やインスタンス変数、メソッドにクラス内部からアクセスする場合、「this」が必須になります。JavaやC#のようにthisを省略することは許されていません。
get/setアクセサを利用する
class ClassName {
get propertyName(): ReturnType {
return returnValue;
}
set propertyName(arg: ArgType) {
}
}
|
get/setアクセサを使うとき、ECMAScript 5から導入された関数を利用するため、プロジェクトの設定やコンパイラーの利用時にその旨、設定する必要があります。tscコマンドを使う場合、$ tsc --target es5 ファイル名
とする必要があります。
インタフェース
interface IHoge {
str: string;
method(): string;
}
class Fuga implements IHoge {
str: string;
method(): string {
return "I'm " + this.str;
}
}
|
TypeScriptでもインタフェースが利用できます。インタフェースはクラスに対して使い、実装を強制させるという、一般的なオブジェクト指向の言語と同様の使い方ができます。TypeScriptではさらに使い方の幅を広げ、変数の型注釈に使うことができます。命名規則の慣習として、プリフィクスに「I」を付ける場合が多いように思いますが、確固たるルールとして確立しているわけではありません。
enum(列挙型)
enum Color {
Red,
Blue,
Yellow
}
var rN: number = Color.Red;
var rS: string = Color[rN];
window.alert(rN + "," + rS); // 0,Red と表示される
|
enumの利用ができます。しかし、Javaなどとは違いenumにメソッドを定義できないため、微妙に使い勝手が悪いです。enumがどのようにJavaScriptに変換されるか、ぜひサンプルを開いてチェックしてみてください。
オブジェクト型リテラル
var objA: { name: string; } = { name: "" };
var objB: { name: string; } = { name: 11 }; // コンパイルエラー!
interface ISample {
name: string;
}
var objC: ISample = { name: "" }; // objA の定義と等価
|
わざわざインタフェースを定義するのが面倒な場合、その場限りの即席の型定義を作り出すことができます。JavaScriptのオブジェクトリテラルに似た構文で型定義を行え、「オブジェクト型リテラル」と呼ばれます。注意点として、オブジェクトリテラルやJSONではプロパティの区切りを「 , 」で行いますが、オブジェクト型リテラルではプロパティの定義の区切りを「 ; 」で行います。一見、対称性がないように思えますが、オブジェクト型リテラルの書き方はインタフェースのボディ部分と全く同一の書き方を行える仕組みになっています。
いろいろな型注釈の書き方
// プロパティシグニチャ
interface ISampleA {
property: string;
}
var objA: ISampleA = { property: "property" };
// コールシグニチャ
interface ISampleB {
(word: string): string;
}
var objB: ISampleB = function(word: string): string {
return "Hello, " + word;
};
// コンストラクタシグニチャ
interface ISampleC {
new (): SampleC;
}
class SampleC {
constructor() { }
}
var objC: ISampleC = SampleC;
var insC: SampleC = new objC();
// インデックスシグニチャ
interface ISampleD {
[index: number]: boolean; // 添字にnumberを使い、booleanを格納できる
}
var objD: ISampleD = {};
objD[1] = true;
// メソッドシグニチャ
interface ISampleE {
method(): string;
}
var objE: ISampleE = {
method: function(): string { return "Hi!"; }
}
objE.method();
// 型としての関数
interface ISampleF {
method: (word: string) => string;
}
var objF: ISampleF = {
method: function(word: string): string { return "Hi! " + word; }
}
objF.method("TypeScript");
|
インタフェースやオブジェクト型リテラルでいろいろなプロパティやメソッドを表現できるように、「プロパティシグニチャ」「コールシグニチャ」「コンストラクタシグニチャ」「インデックスシグニチャ」「メソッドシグニチャ」と呼ばれる書き方が用意されています。
構造的部分型
class Options {
sync: boolean;
}
function doProcess(options: Options): void {
// options型の値を基に何かの処理を行う
}
// 要求通り、doProcess関数にOptions型のインスタンスを渡す
var opts = new Options();
opts.sync = true;
doProcess(opts);
// 求められる性質を満たせば指定された型の直接の値以外も渡せる!
doProcess({
sync: true
});
|
TypeScriptでは「構造的部分型」と呼ばれる考え方があります。求められた型の値に対して、実際に渡す値が型を満たしていれば代用として渡すことができます。これにより、関数やメソッドの引数について、JavaScriptと比べても記述する手間を変えることなく、静的な型チェックが受けられるようになります。
総称型(ジェネリクス)
// Tは型パラメーター
class DataContainer<T> {
data: T;
get(): T {
return this.data;
}
set(value: T): void {
this.data = value;
}
}
// Tをstring型として具体化し、インスタンスを作成する
var strContainer = new DataContainer<string>();
strContainer.set("string1");
window.alert(strContainer.get());
// Tをboolean型として具体化し、インスタンスを作成する
var booleanContainer = new DataContainer<boolean>();
booleanContainer.set(true);
window.alert(booleanContainer.get());
|
TypeScriptでもジェネリクスが利用できます。JavaやC#と大差がないわりに難しい概念なので、ここでは詳細は割愛します。「TypeScriptでは、同一の型パラメーターのリスト内で相互に型パラメーターが参照できない」という不便な制約があることに留意する必要があります(例)。
内部モジュール
module SampleA {
export var str = "string";
}
window.alert(SampleA.str);
// window.alert(str); // SampleAの中で定義したものは他の場所では参照できない
module SampleB {
export class Hoge {
  hello(word: string): string {
  return "Hello, " + word;
  }
  }
  class Fuga { }
export interface IMiyo {
hello(word: string): string;
}
}
module SampleC {
// SampleB.Hoge を Piyoとしてインポート
import Piyo = SampleB.Hoge;
import Fuga = SampleB.Fuga; // exportしていないものは参照できない
import Miyo = SampleB.IMiyo; // インタフェースもimportできる
export var str = new Piyo().hello("TypeScript");
}
window.alert(SampleC.str);
|
JavaScriptでは名前空間を区切るには関数を使ったトリックが必要でした。ですが、TypeScriptではそのトリックを自動で行ってくれる仕組みがあります。それが「内部モジュール」です。これも、サンプルを開いてどのようなJavaScriptコードが生成されているか確認してみるとよいでしょう。
内部モジュールと対をなす「外部モジュール」という仕組みもありますが、Webアプリの開発を行う場合、主に使うのは内部モジュールだけでよいでしょう。
アロー関数式
// 引数にstringを1つ取り、返り値にstringを返す関数
var func: (word: string) => string;
// 見慣れた書き方
func = function(word: string): string { return "Hi, " + word; };
// アロー関数式での書き方
func = (word: string) => "Hi, " + word;
// アロー関数式と中かっこを使った書き方
func = (word: string) => { return "Hi, " + word; };
// アロー関数式は「this」の値を変更しない
class Sample {
name: string;
// stringを返す関数を返す
helloA(): () => string {
return () => "Hello, " + this.name;
}
// stringを返す関数を返す
helloB(): () => string {
return function() { return "Hello, " + this.name; };
}
}
var obj = new Sample();
obj.name = "Instance";
var name = "Global";
// Hello, Instance and Hello, Global と表示される
window.alert(obj.helloA()() + " and " + obj.helloB()());
|
アロー関数式はfunctionを手軽に書くための1つの方法です。関数を値として渡す(コールバックやイベントリスナーなど)頻度の高いJavaScriptでは、かなり便利な機能です。
ただ単に短く書けるだけでなく「this」の値を変更しないため、クラスの中で関数を作りたい場合、通常の関数よりアロー関数式の方が適切に働く場合が圧倒的に多いでしょう。これも、どのようなJavaScriptコードに変換されるか、サンプルを開いて確認してみることを強くお勧めします。
コンストラクタと引数プロパティ宣言
// 引数プロパティ宣言
class SampleA {
constructor(public name: string) {
}
}
// SampleA と等価
class SampleB {
name: string;
constructor(name: string) {
this.name = name;
}
}
var objA = new SampleA("vvakame");
var objB = new SampleB("vvakame");
window.alert(objA.name + ", " + objB.name);
|
コンストラクタの引数にpublicまたはprivateの修飾子を付けることにより、同名のプロパティを宣言し、初期化できるようになります。プロパティをたくさん書かずに済み、コンストラクタの定義と同じ型になるように人力で努力して調整するよりは引数プロパティ宣言を積極的に利用するべきでしょう。
アンビエント宣言
JavaScriptには「型注釈」という考え方や、コンパイル時の静的な型チェックは存在しません。そのため、TypeScriptでJavaScriptコードを利用したいときや、ブラウザー以外の(Node.jsなどの)環境を使いたいとき、本来、実行時は存在するはずなのに型の情報が存在しないため正しく利用できない場合があります。
例えば、underscore.jsなどのライブラリを使いたい場合、「 _ 」という変数は、われわれが明示的に教えてやらない限り、TypeScriptコンパイラーから見ると存在しない型(=エラー)であるように判断されてしまいます。
// 「 _ 」という変数が存在することを教えてやる
declare var _: any;
// コンパイルが通る! 実行時に正しく存在していないと実行時エラーになる
var filteredList: number[] = _.filter([1, 2, 3, 4, 5, 6], function(num){ return num % 2 == 0; });
|
このように、自分が使いたいライブラリや環境についてTypeScriptコンパイラーに存在を教えてやることができます。これは生成されるJavaScriptファイルには影響を与えないため、何らかの方法で実行時に正しく存在するようにしてやる必要があります。
型定義ファイル
型定義ファイルはJavaScriptのライブラリごとにアンビエント宣言を集めたファイルのことで、拡張子は「.d.ts」になります。Playgournd上では型定義ファイルを使ったサンプルが作成できないため割愛します。
型定義ファイルは「DefinitelyTyped」のリポジトリに集積されています。jQueryやBackbone.js、AngularJSやNode.js用の型定義ファイルなど、著名なライブラリは一通りカバーされていますし、日夜増えていっています。興味がある方は筆者が以前Qiitaに書いた記事を参照していただければと思います。
可変長引数
function hello (...words: string[]): string {
return "Hello, " + words.join(" and ");
}
// Hello, JavaScript and TypeScript と表示される
window.alert(hello("JavaScript", "TypeScript"));
|
TypeScriptには可変長引数が導入されています。正直、あまり実装するときには使いませんが、アンビエント宣言で既存のJavaScriptライブラリに型定義を作ってやるときに利用する場合があります。
省略可能引数とデフォルト値付き引数
// ? を付けると省略可能引数になる
function helloA(word?: string): string {
if(word) {
return "Hello, " + word;
} else {
return "Hello, world";
}
}
// = 値 で代入すると値が指定されなかった時のデフォルト値を指定できる
function helloB(word = "world"): string {
return "Hello, " + word;
}
window.alert(helloA());
window.alert(helloA("TypeScript"));
window.alert(helloB());
window.alert(helloB("TypeScript"));
|
省略可能引数とデフォルト値付き引数を定義することができます。
publicとprivate
class Sample {
public strA: string;
private strB: string;
public helloA(word: string): string {
// クラス内部からはprivateな値が利用できる
return this.getPrefix() + word;
}
private getPrefix(): string {
return "Hello, ";
}
}
var obj = new Sample();
obj.strA;
obj.strB; // privateな要素は外部からは参照できない
(<any>obj).strB; // 無理矢理アクセスすればアクセスできるけど……
|
TypeScriptでもクラスの各要素をprivateにすることができます。何も指定しない場合、publicであるものとして扱われます。privateを使うとTypeScriptの仕組みと相性が悪い場合があります。とはいえ、自分が書くコードであれば後からpublicに変えても問題はないため、あまり神経質になる必要はないでしょう。
オーバーロード
function hello(value: number): string;
function hello(value: string): string;
function hello(value: any): string {
if (typeof value === "number") {
return new Array(value + 1).join("Hello!");
} else if (typeof value === "string") {
return "Hello, " + value;
} else {
return "Hello, unknown!";
}
}
window.alert(hello(2)); // Hello!Hello! と表示される
window.alert(hello("world")); // Hello, world と表示される
|
TypeScriptにもオーバーロードはありますが、JavaScriptコード生成の都合上、引数の型ごとに実装を持たせることはできません。そのため、実装を与える宣言はその他のオーバーロードの宣言のどのパターンでも対応できる形にする必要があります。このため、通常の実装時に利用することは少なく、型定義ファイルの作成時に利用する場合が大半です。関数だけでなく、メソッドやコンストラクタなどでもオーバーロードを利用することができます。
型アサーション
var inputA = document.querySelector("#file");
inputA.files; // inputAの型はElement filesプロパティは存在しない
var inputB = <HTMLInputElement>document.querySelector("#file");
inputB.files; // inputAの型はHTMLInputElement filesプロパティが存在する
|
いわゆるキャストです。互換性のある型であれば自由に型付けを変えることができます。型アサーションはむやみやたらに使わず、最小限の利用に抑えるようにしましょう。
型クエリ
class Sample {
}
var Hoge = Sample; // コンストラクタを別の変数に代入
var objA = new Hoge(); // 代入した変数を利用してnewする
// 上記と等価
var Fuga: typeof Sample = Sample;
var objB = new Fuga();
|
型クエリは「型注釈のコピー」とでもいうべき動作を行うためのものです。クラスを定義したとき、クラスのインスタンスの型はクラスと同じになります。では、クラス(コンストラクタ)そのものの型は何になるのでしょうか? TypeScriptでは、“クラスそのものの型”を示す記法は存在しません。そこで、型クエリが役に立ちます。
外部モジュール
export function hello(word: string) {
return "Hello, " + word;
}
export var str = "string";
|
外部モジュールとは、1ファイルを1モジュールと見立てた仕組みのことです。1ファイルが1モジュールなので、トップレベルの要素に「export」を付けます。逆に言うと、トップレベルの要素に「export」を付けた場合、強制的に外部モジュールになってしまいます。外部モジュールはCommonJSのモジュール、またはAMDに対応し、プロジェクトの設定やtscコマンドへ「--module」オプションを渡すことで、どちらの形式で出力するかを選択できます。外部モジュールを使って他のファイルを参照するには、import hoge = require("hoge");
の記法を使います。
Node.js上で動くプログラムを書く場合、外部モジュールの考え方をしっかり理解した方がよいでしょう。外部モジュールを使わない場合でも、内部モジュールと外部モジュールの違いをしっかり理解しておかないと、意図通りの挙動にならないことがままあります。内部モジュールだけを使いたい場合は、トップレベルの要素に「export」を付けないように留意します。
筆者執筆の書籍について
わかめ氏執筆の書籍『TypeScriptリファレンス Ver.1.0対応』が、Impress Japanから発行されていますので、ご紹介します。
2014年5月19日まで発売記念セールが実施されています。『TypeScriptリファレンス Ver.1.0対応』の詳細や購入はImpress Japanのサイトをご覧ください。