プロ生ちゃんと学ぶ! TypeScript入門(3)
TypeScriptの機能と文法、まずはこの3つを押さえよう! 構造的部分型、ジェネリクス、アロー関数式
TypeScriptには多くの機能や文法があり、最新の1.5~1.6でさらに追加された。開発の実践を始める前に、数ある機能の中から最低限、「構造的部分型」「ジェネリクス」「アロー関数式」の3つを押さえておこう。
きよくら ならみ
大都会岡山の片隅でエンタープライズ向けのWebアプリケーションに携わっているソフトウェアエンジニア。
・Microsoft MVP for ASP.NET/IIS
・Twitter: @kiyokura
・Blog: http://kiyokura.hateblo.jp/
暮井 慧
都内の公立高校に通う女子高生。部活は、情報処理研究会。プログラミング生放送のキャラクター「プロ生ちゃん」として活動中!
・Twitter: @pronama
・Website: http://pronama.jp/kei
TypeScript 1.6リリース!
慧 何だかだいぶ時間が経った気がするけど、気のせいかな? さっそく、続きをやろうよ!
き やる気満々だね(笑) あ、そうだ。本題に入る前に、7月22日にTypeScript 1.5、さらに9月17日に1.6がリリースされたので、それにも少し触れておくよ。
慧 新バージョンが出たんだね! どんなふうに変わったの?
き 1.5のアップデートで大部分を占めるのはECMAScript 6(ES6)での機能や構文のサポートだ。その他はnamespace
構文の追加やES6のさらにその先、ECMAScript 7で提案されているdecoratorという機能、コンパイラーのオプションなどが追加されている。
慧 盛りだくさんだね!
き なんだけど、実はここまで説明した内容だと、ほとんど影響を受けない気もするので、詳細は割愛するね(^^;
慧 これから作るなら新しいバージョンで作る方がいいのかな?
き そうだね。これから新しく作り始める場合、開発環境さえ整うなら1.5以降で始めるといいと思う。Atomエディターでatom-typescriptプラグインを使って開発する場合は、パッケージをアップデートしていれば、少なくとも1.5の機能*1が使えるようになっているよ。
- *1 『TypeScript早わかりチートシート【1.5.3対応】』という記事で、TypeScript 1.5で追加された特徴についても解説されていますので、気になる方はこちらもご参照ください。
き さて、本題に入ろうか。そろそろ何かもうちょっと実践的なことをやってみたいのだけど、もう少しだけ、機能的・文法的なところを押さえておこう。ということで、今回は構造的部分型とジェネリクス、アロー関数式について学んでいくよ。
慧 ひょっとして、それが終わったらTypeScriptの勉強も終了!?
き 全然。まだまだ入口だよ(笑)。TypeScriptの機能はもっとたくさんあるんだけど、前回までに加えて、この3つを理解しておくと、『つまみ食い』のレベルがもう一段上がると思うんだ。
構造的部分型
き まずは、構造的部分型からいこう。英語だとStructural Subtyping(ストラクチャラル・サブタイピング)。直訳だね。
慧 英語だと、何か舌かみそう……。
き これは平たく言うと「メンバーの名前と型さえ一致していれば、(その変数が型を継承したり実装したりしていなくても)型チェックを通す」という仕組みだ。いわゆるダックタイプというやつだ。
慧 「アヒルのように鳴くなら、それはアヒルだ!」ってやつだね!
き サンプルコードを見てみよう
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
class Student {
constructor(public id: number, public name: string, public club: string) { }
}
function CheckAttendance(student: Student) {
// Studentと同じメンバーを持った型の値が渡されることを期待する処理
}
// Studentクラスのインスタンスを渡す、通常のパターン
var kei = new Student(1, "慧", "情報処理研究会");
CheckAttendance(kei); // OK
// Studentのインスタンスではないが同じメンバーを持つ、構造的部分型を利用するパターン
var narami = { id: 2, name: "ならみ", club: "テニス" };
CheckAttendance(narami); // OK
// もともとの指定された型に存在しないメンバーがあってもOK
var narami2 = { id: 2, name: "ならみ", club: "テニス", age: 20 };
CheckAttendance(narami2); // OK
// 以下はNGパターン
var ng1 = { id: 3, name: "NGパターン" };
CheckAttendance(ng1); // メンバーにclubが無いのでエラー
var ng2 = { id: "4", name: 555, club: "○○部" };
CheckAttendance(ng2); // メンバーの型が違うのでエラー
|
慧 14~15行目のが構造的部分型だね! 変数narami
はStudentクラスのインスタンスじゃないけど、全く同じメンバーを持ってるから問題なし、と。あ、でも、その下の18行目は、余分なage
っていうメンバーがあるけど、これはエラーにならないんだね!
き そう、余分なものがある分には問題ないんだ。ただ、当然だけど、あるべきメンバーが存在しなかったり、名前が合っていたりしても型が違うとエラーになるよ(22~26行目)。
き この構造的部分型は、動的型付け言語のJavaScriptとの相互利用を考える上で無くてはならない機能だ。これによって動的言語由来の柔軟さを持ちつつもより安全に型を扱うことができる。
慧 動的型付けと静的型付けをうまく取り持つ仕組みだね。スルーしちゃったけど、きよくらさんのage
が20ってのは、ある意味エラーだよ。
ジェネリクス
き 次にジェネリクス(総称型)を紹介するよ。これは型をより抽象化して扱うための機能だ。
慧 JavaやC#にあるものとだいたい同じ考え方でいいのかな?
き そうだね。クラスのメンバーやメソッドの引数や戻り値などの型を、クラスを定義する時点では決定せず、いったん「型変数」と呼ばれる抽象的なもので定義し、利用するときに「型引数(型パラメーター)」で指定することで決定する機能だ。サンプルを見てみて。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
class List<T> {
private data: T[]; // dataの型を型変数Tとして宣言
constructor() {
this.data = [];
}
add(item: T): void { // 引数itemの型を型変数Tとして宣言
this.data.push(item);
}
get(index: number): T { // 戻り値の型を型変数Tとして宣言
return this.data[index];
}
}
var dateList = new List<Date>(); // 型引数としてDate型をセット
dateList.add(new Date()); // addメソッドの引数はDate型となる
var d = dateList.get(0); // getメソッドの戻り値の型もDate型
var nameList = new List<string>(); // 型引数としてstring型をセット
nameList.add("慧"); // addメソッドの引数はstring型
var n = nameList.get(0); // getメソッドの戻り値の型もstring型
// NG
nameList.add(500); // nameListはaddの型はstringなのでエラーになる
|
慧 クラス名の隣に<T>
って書かれているのと、メソッドの引数と戻り値の型が同じT
になってるのがポイントなんだね。16行目では型引数にDate
をセットしてるから、List<T>
の中のT
を全部Date
型に読み替えればいいってわけか~。そして20行目では型引数としてstring
を設定してるから、同じようにT
をstring
に読み替えればOKだね。
き ご明察! ジェネリクスを使うことで、汎用性を持たせながら静的型付けのメリットも享受できる。
慧 :もしジェネリクスが無かったら……さっきの例だとDate
とstring
のそれぞれのクラスを作るか、any
型にするか……になっちゃうもんね(汗
アロー関数式
き どんどんいくよ。次はアロー関数式。
慧 これは私、初めて聞いたよ!
き じゃあ、「ラムダ式」は聞いたことあるかな?
慧 あ、それならC#とJavaで聞いたことあるよ!!
き TypeScriptのアロー関数式は、C#やJavaで言うところのラムダ式のような記法で、シンプルに無形関数を書くことができる。サンプルコードを見てみよう。
var func1 = function (x: number, y: number) { return x + y; };
var func2 = (x: number, y: number) => x + y;
|
き これをコンパイルすると、全く同じJavaScriptコードが出力される(リスト4)。
var func1 = function (x, y) { return x + y; };
var func2 = function (x, y) { return x + y; };
|
慧 アロー関数式の方がちょっとシンプルになるね!
き ぱっと見ると、ちょっとだけのように思えるかもしれないけれど、コールバックやイベントハンドラーなどで無名関数を多用しがちなJavaScriptだと地味に効いてくると思う。また関数オブジェクトの型を宣言するときにも利用できるので、次のようなコードを書くことができるんだ。
// 第3引数にnumber型の引数二つを取り、number型を返す関数をとる関数
function func3(x: number, y: number, logic: (a: number, b: number) => number): number {
return logic(x, y);
}
// number型の引数二つを取り、number型を返す関数としてlogicを宣言する例
var logic: (a: number, b: number) => number;
// logicに関数を代入
logic = (a: number, b: number): number => a * b;
// func3の第3引数にセットしてみる(同じ型なのでOK)
var ret = func3(2, 3, logic);
// 別の実装の関数をセットする例(同じ型なのでOK)
var ret2 = func3(2, 3, (x, y) => x * x * y * y);
|
慧 確かにコードの量が増えると、function
が無くなるだけでも見やすくなるかも。……ちょっと慣れが必要かもだけど、頑張るよ!
き 書いていればすぐ慣れると思うよ。それともう一つ、アロー関数式には忘れてはいけない大きな特徴があるんだ。
慧 なになに!?
き それはthis
の扱いが変わるということ。JavaScriptのthis
の扱いについて説明し始めるとキリがないので割愛するけど、例えばクラスのメンバメソッドをアロー関数式で書くと、自動的にthis
の参照を保存してくれるようなコードにコンパイルされるんだ(リスト6)。
class Hoge {
name: string;
func = (age: number) => {
return this.name + ":" + age.toString();
};
}
|
var Hoge = (function () {
function Hoge() {
var _this = this; // thisの参照を _thisとして保存している
this.func = function (age) {
return _this.name + ":" + age.toString(); // 保存された _this を参照している
};
}
return Hoge;
})();
|
慧 うう、ちょっと難しいかな(汗
き これはJavaScript自体のthis
の扱いについて知らないと、なかなか理解できないかもしれないね。誤解を恐れずに言うと、クラス構文の中でアロー関数式を使う場合、そのアロー関数式の中ではC#やJavaと同じような感覚でthis
を使っても問題ない、という感じかな。
慧 C#やJavaでもラムダ式はもう当たり前の機能だし、慣れておくのも悪くないよね!
既存のJavaScriptライブラリの相互利用のための機能
き ここらで言語の文法的なところについて触れるのはいったん終わろう。
慧 いよいよ実践的な内容に入るんだね!
き うん!……と言いたいところなんだけど、ちょっとだけ待って(笑)
慧 えー!
き まあまあ。実際にTypeScriptを使って開発を行おうとすると、既存のJavaScriptライブラリを利用する局面が多くなると思うんだ。
慧 そうだね、jQueryとか使いたいよね
アンビエント宣言
き 何度も言っているように、TypeScriptはJavaScriptと相互利用を前提に考えられている。とはいえ、JavaScriptのライブラリをTypeScriptから利用しようとすると、どうしても問題に突き当たってしまう。何か分かるかな?
慧 うーん、……型の情報が無いことかな?
き 正解! TypeScriptは静的型付け言語なので、コンパイル時に型の情報が必要になる。型推論などの機能があるものの、それだけで全てまかなえるというわけじゃない。
慧 じゃあどうするの?
き TypeScriptにはそのための機能がある。それがアンビエント宣言だ。具体的には次に示すように、declare
キーワードに続けて、型の情報を書いていくことになる。
// 変数の宣言
declare var jsLibVar: number;
//関数の宣言
declare function jsLibFunc(arg1: number): number;
// インターフェース
declare interface IJsLibIF {
name: string;
age: number;
}
// クラス
declare class JsLibClass {
constructor(arg: number);
hoge: string;
func(arg1: number): number;
}
// アンビエント宣言された識別子を参照してみる
var lib = new JsLibClass(jsLibFunc(jsLibVar)); // エラーにならない
|
慧 JavaScriptにはどういうふうにコンパイルされるの?
き これはJavaScriptにはコンパイルされないよ。あくまでもコンパイルする際に必要な型の情報として参照されるだけなんだ。実体はすでにどこかにあるJavaScriptのソースコードで、それをTypeScriptから利用するためのものだからね。
慧 なるほど。C言語のプロタイプ宣言みたいなものと思っていいのかな?
き そうそう、そんな感じ……って、慧ちゃん、C言語も分かるんだね(汗
型定義ファイル
き JavaScriptライブラリの型情報はアンビエント宣言を行うことで解決できるけど、毎回その都度、ソースコードに書いていくのはあまり現実的じゃあないよね。
慧 それは面倒くさいね。
き TypeScriptでは、このアンビエント宣言をまとめた型定義ファイルを作って利用することができる。この型定義ファイルは.d.ts
という拡張子で保存する決まりになっている。
慧 C言語でいえばヘッダファイルみたいな感じかな?
き そうそう、そんな感じでとられても問題ないと思うよ。
慧 この型定義ファイルはどうやって手に入れたらいいんだろう? もしかして自分で作らないといけないの??
き 最近はライブラリの提供元が.d.tsファイルも併せてリリースするものも出てきているけど、多くはそうではないね。でも大丈夫。有志で.d.tsファイルを集めてメンテナンスしているところがあるんだ。DefinitelyTypedという、github上のリポジトリだ。
き よく使われるような著名なライブラリなら、大抵そろってるんじゃないかな。
慧 じゃあ安心だね!
次回はJavaScriptのライブラリを使ってみるよ
き じゃあ、さっそくいくつかJavaScriptのライブラリを使って……と思ったんだけど、気が付いたら結構時間がたっているね。キリがいいし、ちょっと休憩しようか。
慧 えー! またお預けー?
■
Build Insider × プロ生ちゃんの壁紙ダウンロード 第2弾
プロ生サイトで特別壁紙がダウンロードできます。
1. TypeScriptってどんなもの? プロ生ちゃんと始めてみよう!
TypeScriptが気になる人は、本連載で楽しく優しく学ぼう。TypeScriptの特徴から、Atomエディター開発環境の構築まで紹介。
2. TypeScriptの基本のキ。JavaScriptをベースに「TypeScriptをつまみ食い」しよう!
TypeScriptの基本中の基本である「型」「クラス」「インターフェース」をプロ生ちゃんと学ぼう。「今、TypeScriptやって損しない?」という疑問・不安についても回答する。