TypeScriptを使ってECMAScript 2015時代のコードを書く 3
第一回は、「なぜTypeScriptか?」という話を書きました。前回はTypeScriptで開発する際の環境についてがテーマでした。今回は、最後の締めくくりとしてECMAScript 2015において積極的に利用するべき構文を紹介していきます。
まず、TypeScriptの近況をお伝えしておきます。TypeScriptの現在のバージョンは1.7.5で、この後もまだまだアップデート予定は詰まっています。アップデートの予定はたくさんありますが、これは必ずしも機能が十分に揃っていないということを意味しません。
第一回で触れたとおり、TypeScriptはECMAScript 2015のsuperset+型のための構文が導入された言語です。TypeScriptの今後のアップデートはTypeScript自体の使い勝手や、型の柔軟性を改善するためのものが大部分。その証左として、1.5.3より後の1.6.2、1.7.5のアップデートではECMAScript 2015の構文の追加はGeneratorsくらいのものです。つまり、TypeScriptはすでにECMAScript 2015による記述を行うためには十分成熟している、といえます。
次に挙げる機能を使う場合、多くのブラウザで動作させるために何らかの後処理が必要になります。
- Modules
- ECMAScript 2015としてのmoduleの仕様は決まりましたが、ブラウザ上で動かせる状態にはありません。そのため、require.jsやbrowserify、SystemJSなどのツールが必要。
- なお、Node.js上で動作させる場合はCommonJSスタイルのJavaScriptとして出力できるためTypeScriptコンパイラによる変換だけで利用可能。
- Generators
--target es6
でのみ利用可である(es5で動く形式に変換できない)ため、利用したい場合はbabelによる変換が必要。
また、仕組み上es5で動く形に変換ができない(つまり、全くの新機能である)ものもいくつかあります。ブラウザなどの進歩を待ちましょう。
本稿では、すべてのサンプルコードは現代的に、つまりECMAScript 2015的に書くことにします。古くからのJavaScriptユーザとしては見慣れないかもしれない、そして別の言語に慣れている人には、比較的読みやすい記法でしょう。
これから紹介するTypeScript独自の記法ではないものは正式にJavaScriptの仕様になったものであるため、今後数年以内に徐々にすべてのJavaScriptコードが本稿で紹介されるような記法へと移行されていく…といいなぁ。
また、本稿に出てくるサンプルコードはTypeScript playgroundですぐに試せるリンクをなるべく添えています。Playground上で、変数名などにマウスカーソルをホバーさせるとどういう型として解釈されているか確認できるのでいろいろ試してみてください。Playgroundはmodule周りのサポートがすっぽり抜け落ちているので、その辺りは割愛しています。前回の記事を参考に、自分で環境を整え、試してみてください。
TypeScript独自の構文
まずは、TypeScript独自の構文について述べておきます。独自の構文といっても、型に関する部分についてなのでプログラムの動作には影響しません。プログラムの堅牢さ、分かりやすさ、保守しやすさによい影響があります。
詳しく知りたい場合は、筆者の書いた別の解説やTypeScriptの更新差分解説を参照してください。
型注釈の基本
TypeScript最大の特徴として、型の存在とそれを利用した静的なチェックがあります。
JavaScriptにももちろん型は存在しています。string, number, Date, RegExpなどなど多岐に渡りますがJavaScriptでは変数と型を結びつけるということをしませんでした。
TypeScriptでは変数に型を明示的に指定(注釈を与える)ことにより、静的なチェックやIDEによる支援を受けることができます。
変数や引数について型の指定ができる箇所は多くありますが、基本的に次の箇所だけマークしておけば多くの局面に対応できます。
- 初期化なしの変数宣言
- 関数の仮引数
- 関数の戻り値の型
- 型パラメータ
TypeScriptでは、変数や仮引数などの後に: 型名
という形式で記述します。number, boolean, string, void, anyなど、組み込みの型と、DateやRegExpなど、見知ったJavaScriptのオブジェクトも存在しています。
"use strict";// 関数の仮引数と戻り値の型 function concat(a: string, b: string): string { return a + b; }
// 変数の型 let obj1: RegExp; // 初期化子がある場合、わざわざ型注釈を明記しなくとも自動的に推論される let obj2 = /^Hi!$/;
let array1: Array; // array1 と全く同じ意味合い let array2: boolean[];
簡単ですね。型パラメータの考え方は、JavaやC#を知っている人であれば、馴染みのある記法でしょう。TypeScriptは配列だけは特別扱いをしており、Array
などの記法の他に、boolean[]
という記法をも許しています。これも、JavaやC#を知る人であれば、見たことがある記法かもしれません。TypeScriptでは、配列についてはboolean[]
の記法を使うほうが一般的です。
Interfaces
インタフェースは、あるオブジェクトについての性質を記述し、それに名前をつけることができます。JavaやC#ではあるクラスに特定の実装を強制するものとして使いますが、TypeScriptではその範囲にとどまりません。
クラス化するほどでもない、サーバから受け取るデータについて名前をちょいちょいと与える場合などに使われる場合が多いです。
"use strict";interface Sample { str: string; func: (n: string) => number; methodA(): void; }
let obj: Sample; obj.str; obj.func("test"); obj.methodA();
ECMAScript 2015
さて、それではECMAScript 2015の構文の説明に入っていきます。筆者が育てている4つの個人リポジトリのソースコードを見て、実際に使っているもののみを抜粋してきました。
つまり、ここで紹介している構文は使い勝手がよく、ECMAScript 2015以前よりも改善されている構文である!といえます。
use strict
まずは、 “use strict”; について紹介しておきます。これはECMAScript 5のときからあるディレクティブです。
"use strict";// getterのみあり setterなし let obj = { get str() { return "Hi!"; } };
// エラーにしてくれる! // Uncaught TypeError: Cannot set property str of # which has only a getter // "use strict"; がないと、単に無視されるので値がセットされない謎のバグになり苦しみのたうつ obj.str = "setterは存在していない";
多くの効能があるため、ここでは詳細には解説しません。ここで解説した理由は、ECMAScript 2015の仕様上、後述のクラスやモジュールの内部は暗黙的にstrict modeになるためです。TypeScriptで書いたコードをコンパイルするとクラスやモジュールの構文も変換されてしまい、暗黙的にstrict modeではなくなってしまいます。
そのため、後々ECMAScript 2015が無変換で動く時代になった時に困らぬよう、今のうちからすべてのファイルの先頭に"use strict";
を書いておき、挙動を揃えておきます。これはトイレに行った後に手を洗うというレベルのエチケットなので、tslintなどを活用して忘れないように気をつけましょう。
let, const
let, constを紹介します。JavaScriptではvar
というキーワードを使って変数定義をしていました。let, constではvarに対して次の違いがあります。
- ブロックスコープである
- 同じ変数名を複数回定義しようとするとエラーになる
- 変数の巻き上げは行われるが宣言前にアクセスするとエラーになる
constの場合、さらに追加があります。
- 宣言時に初期化必須
- 値の置き換え不可
便利ですね。varの不思議な挙動について理解しておかねばならない時代も過去のものとなりそうです。多くの場合で単純にvarをletに置き換えできるし、そうしたほうがよいでしょう。varをletに置き換えて動かなくなるようなコードは、JavaScriptの不快な仕様を利用した悪いコードだと考え、リファクタリングするべきです。
"use strict";let objA = "Aだよ!"; // ブロックスコープだ! varはブロックスコープではない { let objA = "Aだけど2番目だよ!"; // Aだけど2番目だよ! と表示される console.log(objA); } // Aだよ! と表示される console.log(objA);
// 2重に宣言してもエラーにならない var objB: any; var objB: any;
// 2重に宣言するとちゃんとエラーになる(TypeScript上でもプログラム実行時でも) let objC: any; let objC: any;
// 定数として宣言すると値の書き換えはエラーになる(TypeScript上でもプログラム実行時でも) const objD = "Hi!"; objD = "Bye!";
// ちなみに値のさらに中身を書き換えるのは問題ない const objE = { str: "Hi!" }; objE.str = "Bye!"; // 対策(実行時エラーになる) const objF = Object.freeze({ str: "Hi!" }); objF.str = "Bye!";
なお、TypeScriptで--target es5
などでコンパイルした時はletがvarに変換されるため、実行時エラーは発生しなくなってしまいます。実用上はコンパイル時にそれと分かればとりあえずは問題ないでしょう。
Classes
オブジェクト指向な言語であればだいたい存在するクラスです。JavaScriptではECMAScript 2015から導入されました。ECMAScript 5までは、prototypeという仕組みを使ってオブジェクト指向なコードを書いていました。
prototypeにも良いところがあり、悪いモノでもないのですが、広く知られているという分かりやすさ、関連する記述がひとところにまとまっているという利点を考えると、やはりclassが使えるというのはありがたいものです。
今後、prototypeの出番は大きく減るでしょう。とはいえ、classの裏側はprototypeであるため、今まで使えたハックも利用可能ではあります。
"use strict";class Sample { name: string;
constructor(name: string, public extention = "!") { this.name = name; } hi(): string { return `Hi, ${this.name}${this.extention}`; }
}
let obj = new Sample("TypeScript"); // Hi, TypeScript! と表示される console.log(obj.hi());
JavaやC#を見慣れているユーザであれば、大変親しみやすいコードになったと思います。ぜひ、PlaygroundでどういうES5なコードに変換されるかを確認してみてください。これを手書きで書くのは、チーム内の人数が多くなれば多くなるほど辛く感じるでしょう。
また、クラスが使えるようになりましたが、だからといってなんでもかんでもクラスにしなければならないわけではありません。今までどおり、関数や普通のオブジェクトを使うのが適していると思った場合はどんどん使って構いません。無理にクラスを使おうとするほうが、意図のわかりづらい、よくないコードになってしまうでしょう。
TypeScriptのみの独自仕様として、constructorでpublic extention = "!"
のように、アクセス修飾子付きで仮引数を宣言すると、自動的に自分のプロパティとしてセットされます。ECMAScript 2015の仕様に準拠したコードを書きたい場合は、name
プロパティのように、フィールドの宣言をしてからthis.name = name;
のように自分で代入しましょう。
気になる場合は、tscでのコンパイル時に--target es6
を指定してコンパイルし、どのようなpure ECMAScript 2015コードが生成されるかをチェックしてみるとよいでしょう。
Modules
モジュールです。ECMAScript 2015では、ついにモジュールという概念が、JavaScriptの仕様として盛り込まれました。今までのJavaScriptにはモジュールの仕様が正式にはなく、AMDやCommonJSなど、それを埋めるための(ECMAScriptではない)仕様が複数考えだされ、実装されてきました。Node.jsではCommonJSのモジュールに仕様に近いものが採用されています。
ECMAScript 2015でのモジュールの仕様は、ざっくりいうと 1ファイル=1モジュール という仕様です。1ファイル毎に名前空間が分割されていて、どの識別子を外部に公開するかを選択していきます。
モジュールの仕様は、静的に(つまり実行することなく)ソースコード間の依存関係が解決できるようになっています。そのため、今までのJavaScriptのコードとはかけ離れた、全く新しい構文をもっています。
コード例を見ていきましょう。utilA.ts, utilB.ts, main.ts という3つのコードを利用します。3ファイルあるので、モジュールが3つあることになります。
"use strict";export default function() { return "Hi! from utilA"; }
"use strict";export function hi() { return "Hi! from utilB"; }
"use strict"; // es6 moduleを使うと自動的に use strict した事になる。 // しかし、TypeScriptでコンパイルするとそうではなくなってしまうので明示的に指定しておく。import utilA from "./utilA"; import {hi} from "./utilB"; import * as utilB from "./utilB";
// Hi! from utilA と表示される console.log(utilA()); // Hi! from utilB と表示される console.log(hi()); // Hi! from utilB と表示される console.log(utilB.hi());
残念ながら、Playgroundで試すことができないので、お手元に環境を作って試してみてください。
exportして、importするだけの仕様ではありますが、慣れるまでは正しい記法がわかりづらく戸惑いを覚えるでしょう。TypeScriptの利点として、動作しない書き方をした場合きっちりコンパイルエラーにしてくれるので試行錯誤する手間が省けます。
Arrow functions
Arrow functions(アロー関数)です。ECMAScript 5までのJavaScriptの関数は、thisの値が他言語ユーザの期待とだいぶかけ離れた動作をしていて辛かったです。アロー関数では期待する挙動と実際の挙動の齟齬がだいぶ軽減されています。
"use strict";// 引数なしの場合 括弧は必要 let funcA = () => { console.log("Hi!"); }; // Hi! と表示される funcA();
// 引数1個の場合、括弧をつけることもできる let funcB = (name: string) => { console.log(
Hi! ${name}
); }; // Hi! TypeScript と表示される funcB("TypeScript");// 引数が1個の場合、括弧を省略して仮引数名のみを書くことができる // ただし、括弧を省略する場合仮引数の型を指定することはできない // このため、ここではfuncC変数に型を指定している let funcC: (name: string) => void = name => { console.log(
Hi! ${name}
); }; // Hi! TypeScript と表示される funcC("TypeScript"); // 既に型がついている場合そのまま省略できる funcC = name => { console.log(Hi! ${name}
); }; // Hi! TypeScript と表示される funcC("TypeScript");let funcD = (name: string, ...rest: string[]) => { console.log(
Hi! ${name} and ${rest.join(", ")}
); }; // Hi! TypeScript and ECMAScript 2015, ECMAScript 2016 と表示される funcD("TypeScript", "ECMAScript 2015", "ECMAScript 2016");
この使い方では、EMCAScript 5の関数と大差ありませんね。タイプする文字数が減って嬉しいですが、まあ、その程度です。名前付きアロー関数は作ることができませんので、素直に変数に代入しましょう。
次の例を見てみましょう。
"use strict";class Sample { constructor(public name: string) { }
toString(): string { return `${this.name}!`; } method() { // ここはmethodのトップレベルです let funcA = () => { // arguments にアクセスできない // thisの値はmethodのトップレベルと変わらない console.log(`Hi, ${this}`); }; let funcB = function() { // arguments にアクセスできる // thisの値は undefined console.log(`Hi, ${this}`); }; // Hi, TypeScript! と表示される funcA(); // Hi, undefined と表示される funcB(); }
}
let obj = new Sample("TypeScript"); obj.method();
アロー関数の特徴が顕著に出ていますね。
理由が明確に説明できない限り、今後はfunctionと記述する関数ではなく、アロー関数を使うべきでしょう。具体的に、今までのfunctionを使うのは名前付き関数を作りたい場合や、thisの値を変更するタイプのAPIをもつライブラリを使う場合などです。
Default parameters, Rest parameters, Spread operators
Default parameters(デフォルト値付き引数)、rest parameters(可変長引数)、Spread operators(展開演算子)について取り上げます。これらもECMAScript 2015からの導入になりますが、特にお世話になるのはデフォルト値付き引数でしょう。
"use strict";// デフォルト値付き引数 // 当てはまる引数が渡されない場合、指定された値がデフォルト値として利用される function hi(name = "TypeScript") { return
Hi! ${name}
; } // Hi! TypeScript と表示される console.log(hi()); // Hi! ECMAScript 2015 と表示される console.log(hi("ECMAScript 2015"));// 可変長引数 function bye(...names: string[]) { if (names.length === 0) { return
Bye, JavaScript
; } returnBye, ${names.join(", ")}
; } // Bye, JavaScript と表示される console.log(bye()); // Bye, JavaScript, ECMAScript 3 と表示される console.log(bye("JavaScript", "ECMAScript 3"));function goodOldDays() { return ["JavaScript", "ECMAScript 3", "ECMAScript 5"]; }
let array = goodOldDays(); // spread(展開)される // Bye, JavaScript, ECMAScript 3, ECMAScript 5 と表示される console.log(bye(...array)); // 同上 console.log(bye(array[0], array[1], array[2]));
ECMAScript 5までは、p = p || "TypeScript";
のような、JavaScriptユーザ以外にはわかりにくいイディオムを取る必要がありましたがだいぶわかりやすくなりましたね。
Template string
Template stringは文字列の生成が今までより簡単にできるようになりました。これにより、文字列を結合してメッセージなどを生成するのが格段に読みやすくなります。Gruntfile.jsやgulpfile.jsでできると、設定ファイルも大変見やすく保守も簡単になります。
"use strict";let name = "TypeScript"; // Hi! TypeScript と表示される console.log(
Hi! ${name}
);name = "ECMAScript 2015" // Hi! ECMAScript 2015 と表示される console.log(
Hi! ${name}
);// Hi! ECMAScript 2015 と表示される console.log("Hi! " + name);
まさにテンプレートですね。
Tagged template stringという機能もあるのですが、あまり有効な使いみちがないのと、活用した有名なライブラリなども登場してきていないので入門レベルでは気にする必要はないでしょう。
Promises
ECMAScript 2015から、非同期処理における標準のAPIが定義されました。それがPromiseです。Promiseは「約束する」の名の示すとおり、将来的に処理結果を返すことを約束し、その予約表を先に払い出すイメージです。Promiseの結果は成功か失敗の2状態で表されます。
"use strict";let p1 = Promise.resolve(true); p1.then(value => { // true と表示される console.log(value); });
let p2 = Promise.reject("何かのエラー!"); p2.then(value => { // rejectの場合はここに到達しない console.log(value); }, err => { // 何かのエラー! と表示される console.log(err); });
let p3 = Promise.reject("何かのエラー!"); p3.catch(err => { // 何かのエラー! と表示される console.log(err); });
let p4 = new Promise((resolve, reject) => { let xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState == 4) { if (xhr.status == 200) { resolve(xhr.responseText); } else { reject(xhr); } } } xhr.open("GET", "http://www.typescriptlang.org/"); xhr.send(); }); p4 .then(text => { // 通信が成功していたらHTMLがどばー!と表示される console.log(text); }) .catch((xhr: XMLHttpRequest) => { // 通信に失敗していたらステータスコードが表示される console.log(xhr.status); });
Playgroundは--target es5
で動作しているため、Promiseの型定義が存在していません。コンパイルエラーが出ていてもRunすることはできますが、できれば手元で試してみるのがよいでしょう。
Promiseは今後さまざまな標準APIの一部として使われることになります。ブラウザ上で動くコードを書いている場合でも、ServiceWorkerやfetch APIなどを使うようになると、まず間違いなく触らなければいけなくなるため、今のうちに確実にマスターしておくことをお勧めします。azuさんのPromise本が大変わかりやすいので熟読すると得るものが大きいと思います。
まとめ
いかがでしょうか。わかりやすく使いやすい変更が多いので、今すぐにでも使い始められると思います。まずは、既存コードなしの100% TypeScriptなコードから書き始め、徐々に適用範囲を広げていってみてください。
CoffeeScriptやECMAScript 5なコードからは遅かれ早かれ移行しなければならないと思いますので、本記事を参考に徐々に実践を始めてみてください!それでは良いTypeScript & ECMAScript 2015ライフを!