TypeScriptの基礎と文法
TypeScript早わかりチートシート【1.5.3対応】
TypeScript 1.5正式リリース。最新言語仕様を速習しよう! TypeScriptを使うときに役立つ情報がまとまったチートシート(1クリックで試せるサンプル付き)。
本稿はTypeScript 1.5.3を対象に解説を行います(※2016/11/08追記:姉妹サイトの@IT Insider.NETで「特集:TypeScript 2.0概説」 を掲載していますので、本稿と併せてご参照ください )。
早いもので、TypeScript 1.0がリリースされた2014年4月3日から、1年以上が経ちました。今年の頭あたりに本記事のTypeScript 1.0版からの更新の依頼が来た時は、(記事改訂は想定していなかったので)びっくりしました。情報をメンテし、良い情報がWeb上に残るよう運営するのは大変であろうな、と想像に難くないですが、筆者としてはうれしい限りです。
さて、去る2015年3月5日、TypeScriptにとって大きな転機とも言える出来事がありました。Angular 2.0でのTypeScriptの正式採用の知らせです*1。AngularJSを作るグーグルと、TypeScriptを作るマイクロソフト、2人の巨人が手を結んだ形になります。今後、TypeScriptはより広く使われるようになり、将来的にはECMAScript時代にその仕様が取り込まれていくに違いありません。
- *1 もともと、グーグルはAtScriptという、TypeScriptのsuperset(=上位セット)となる言語を作成し、それを用いてAngular 2を実装すると宣言していました。しかし、それを止め、代わりにTypeScriptそのものを使うように方針転換したのは、1つにTypeScriptチームと協調し、Angular 2に必要な機能がTypeScriptの言語仕様に入る合意が取れたという理由があります。そして、もう1つの要素として、TypeScriptのあまりに活発かつ勢いのよい開発を受け、それのsupersetを作るには莫大な労力がかかる! と、気が付いたからだと筆者は考えています。
さて、このような形で近年のAltJS(=JavaScriptの代替言語)戦争の中で一歩抜きん出る形になったTypeScriptをぜひ使い始めてみましょう。
本記事ではTypeScriptを使うときに役立つ情報をチートシートとしてまとめます。サンプルプログラムが必要な箇所には「TypeScript Playground」を利用し、1クリックで試すことができるリンクを示します。
TypeScriptの1.5.3をベースに解説し、コンパイル時のターゲット設定はes6
(ECMAScript 6)とします。コンパイル時のターゲット設定をes5
またはes3
にした場合、多くのes6用の機能は上手くダウンコンバートされ、そのまま動作させることができます。
本稿で解説しているTypeScript言語仕様一覧は以下の通りです。
- TypeScriptの基本的な言語仕様: 変数の型注釈と型推論 | クラスを利用する | get/setアクセサを利用する | インタフェース | enum(列挙型) | オブジェクト型リテラル | いろいろな型注釈の書き方 | 構造的部分型 | 総称型(ジェネリクス) | namespace(内部モジュール) | アロー関数式 | コンストラクタと引数プロパティ宣言 | アンビエント宣言 | 型定義ファイル | 可変長引数 | 省略可能引数とデフォルト値付き引数 | publicとprivateとprotected | オーバーロード | 型アサーション | 型クエリ | 外部モジュール | タプル型 | 共用型 | type alias(型の別名)
- TypeScriptとECMAScript 2015(6)、7: let/const | template literals | shorthand properties | destructuring | spread operator | ES6 modules | for...of | symbols | computed properties | decorators
TypeScriptの位置付け
まずは、TypeScriptがJavaScriptに対してどういう位置付けにあるかを解説します。TypeScriptの位置付けを知ることで、JavaScriptの知識をどう活用できるか、どう参考にできるかが分かります。
JavaScriptの仕様はECMAScript 5.1としてまとめられています。TypeScriptは、JavaScript+静的型付け言語に変身するための仕様を詰め込んだ言語です。ECMAScript 5.1の仕様を全て受け継ぎつつ、現在策定中のECMAScript 6th Edition(ESCMScript 2015)の要素も意欲的に取り込み続けています。
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!
// es6 では symbol もプリミティブ型!
var s: symbol = Symbol.for("s");
|
TypeScriptには変数や関数などに型が存在していて、明示的にどの型を使うかを指定できます(型注釈)。値からの型推論も行えるため、変数の定義と代入を同時に行うようにすると、JavaScriptと変わらぬ記述性と堅固な型チェックの両方の恩恵を受けることができます。
また、string
、number
、boolean
の3種類は、プリミティブな型として最初から利用できます。es6では、symbol
もプリミティブな型の1つと数えられます。
クラスを利用する
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 ファイル名
など、--target
オプションにes5かes6を指定する必要があります。
インタフェース
interface Hoge {
str: string;
method(): string;
}
class Fuga implements Hoge {
str: string;
method(): string {
return "I'm " + this.str;
}
}
|
TypeScriptでもインタフェースが利用できます。インタフェースはクラスに対して使い、実装を強制させるという、一般的なオブジェクト指向の言語と同様の使い方ができます。TypeScriptではさらに使い方の幅を広げ、変数の型注釈に使うことができます。
命名規則として、先頭にI
を付けていた時期もありましたが、現在ではマイクロソフトのガイドライン上で、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コードに変換されるか、ぜひサンプルを開いてチェックしてみてください。
また、const enum
と呼ばれるコンパイル時に展開されるenumがTypeScript 1.4から導入されました。
const enum Permission {
Read = 4,
Write = 2,
Execute = 1,
}
var filePermission = Permission.Read + Permission.Write + Permission.Execute;
window.alert(filePermission);
|
このように、enum形式で書いたものが即値として展開されるため、実行時のペナルティが少なくなります。コンパイル時に--preserveConstEnums
オプションを与えることで、const
ではない、通常のenum
と同様のコードを出力させることも可能です。Playgroundで変換後のJavaScriptコードを確認してみてください。
オブジェクト型リテラル
var objA: { name: string; } = { name: "" };
var objB: { name: string; } = { name: 11 }; // コンパイルエラー!
// インタフェースの {} 内部と同じ書式です。
interface Sample {
name: string;
}
var objC: Sample = { name: "" }; // objA の定義と等価
// TypeScript 1.5から ; ではなく , で区切ってもOK!
var objD: { name: string, like: string } = { name: "vvakame", like: "お肉" };
|
わざわざインタフェースを定義するのが面倒な場合、その場限りの即席の型定義を作り出すことができます。
この即席の型の記述を「オブジェクト型リテラル」と呼びます。オブジェクト型リテラルの書き方は、インタフェースのボディ部分と全く同一の書き方で書くことができます。
オブジェクトリテラルやJSONではプロパティの区切りを,
で行います。それと同様に、オブジェクト型リテラルでも,
で区切ることができます。
現時点では;
と,
のどちらを使うのがよいか、明確な指針はありません。
いろいろな型注釈の書き方
// プロパティシグニチャ
interface SampleA {
property: string;
}
var objA: SampleA = { property: "property" };
// コールシグニチャ
interface SampleB {
(word: string): string;
}
var objB: SampleB = function(word: string): string {
return "Hello, " + word;
};
// コンストラクタシグニチャ
interface SampleCIF {
new (): SampleC;
}
class SampleC {
}
var objC: SampleCIF = SampleC;
var insC: SampleC = new objC();
// インデックスシグニチャ
interface SampleD {
[index: number]: boolean; // 添字にnumberを使いbooleanを格納できる
}
var objD: SampleD = {};
objD[1] = true;
// メソッドシグニチャ
interface SampleE {
method(): string;
}
var objE: SampleE = {
method: function(): string { return "Hi!"; }
}
objE.method();
// 型としての関数
interface SampleF {
method: (word: string) => string;
}
var objF: SampleF = {
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では、同一の型パラメータのリスト内で相互に型パラメータが参照できない」という不便な制約があることに留意する必要があります(例)。
namespace(内部モジュール)
namespace sampleA {
export var str = "string";
}
window.alert(sampleA.str);
// window.alert(str); // SampleAの中で定義したものは他の場所では参照できない
namespace sampleB {
export class Hoge {
hello(word: string): string {
return "Hello, " + word;
}
}
class Fuga { }
export interface IMiyo {
hello(word: string): string;
}
}
namespace 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);
// TypeScript 1.5.0-beta までは以下の書き方だった。今も使えるが、なるべくnamespaceを使う。
module sampleD {
}
|
JavaScriptでは名前空間を区切るには関数を使ったトリックが必要でした。ですが、TypeScriptではそのトリックを自動で行ってくれる仕組みがあります。それが「namespace(内部モジュール)」です。TypeScript 1.5.0-betaまでは、内部モジュールという名前で、module
という書き方を使っていました。しかし、ECMAScript 6で別途moduleという仕組みが登場してきたため、混乱を避けるためにnamespace
という新しい書き方がTypeScript1.5.3からできるようになっています。今後は、なるべくnamespace
を使っていくのがよいでしょう。これも、サンプルを開いてどのようなJavaScriptコードが生成されているか確認しておくとよいでしょう。
内部モジュールと対をなす「外部モジュール」という仕組みもありますが(後述)、Webアプリの開発を行う場合、主に使うのはnamespace
(内部モジュール)だけでよいでしょう*2。
- *2 最近はBrowserifyなどの台頭により、外部モジュールを使ってコードを書き、Browserifyで処理して使うワークフローも考えられます。
アロー関数式
// 引数に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とprotected
class Sample {
public strA: string;
private strB: string;
protected strC: string;
public helloA(word: string): string {
// クラス内部からはprivate、protectedな値が利用できる
this.strC = this.getPrefix() + word + this.getPostfix();
return this.strC;
}
private getPrefix(): string {
return "Hello, ";
}
protected getPostfix(): string {
return "!";
}
}
class Sub extends Sample {
public helloB(word: string) {
this.helloA(word);
return this.strC + this.getPostfix();
}
}
var obj = new Sample();
obj.strA;
obj.strB; // privateな要素は外部からは参照できない
(<any>obj).strB; // 無理矢理アクセスすればアクセスできるけど……
var sub = new Sub();
// Hello, TypeScript!! と表示される
console.log(sub.helloB("TypeScript"));
|
※なお、本稿でconsole.log
メソッドによりログ出力しているものは、Chromeに付属の開発者ツールの[Console]タブで出力結果を参照できます。
TypeScriptでもクラスの各要素をprivate
にすることができます。何も指定しない場合、public
であるものとして扱われます。private
を使うと、TypeScriptの仕組みと相性が悪い場合があります。とはいえ、自分が書くコードであれば、後からpublic
に変えても問題はないため、あまり神経質になる必要はないでしょう。
TypeScript 1.3からprotected
が使えるようになりました。子クラスを定義したときに、子クラスからは親クラスのprotected
な要素にアクセスできるようになります。
オーバーロード
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、UMD、SystemJSに対応し、プロジェクトの設定やtsc
コマンドへ--module
オプションを渡すことで、いずれかの形式で出力するかを選択できます。外部モジュールを使って他のファイルを参照するには、import hoge = require("hoge");
の記法を使います。
Node.js上で動くプログラムを書く場合、外部モジュールの考え方をしっかり理解した方がよいでしょう。外部モジュールを使わない場合でも、内部モジュールと外部モジュールの違いをしっかり理解しておかないと、意図通りの挙動にならないことがままあります。内部モジュールだけを使いたい場合は、トップレベルの要素にexport
を付けないように留意します。
タプル型(tuple types)
var tuple: [number, string] = [1, "Hi!"];
// 1番目はnumberと指定されている
tuple[0].toFixed();
// 2番目はstringと指定されている
tuple[1].charAt(0);
// numberにはcharAtメソッドは存在しないのでコンパイルエラー!
tuple[0].charAt(0);
|
TypeScriptにも1.3からタプル型が入りました。しかし、一般に使われるタプルとは違い、ただの配列に特殊な型注釈の書き方を用意しただけなので、あまり便利ではありません。既存のJavaScriptライブラリに対して、型定義を書くときに使えるかな? といったところです。自分で新しくコードを書くときに使うことはあまりないでしょう。
共用型(union types)
function fizzBuzz(v: number): number | string {
if (v % 15 === 0) {
return "FizzBuzz";
} else if (v % 3 === 0) {
return "Fizz";
} else if (v % 5 === 0) {
return "Buzz";
} else {
return v;
}
}
var results: (number | string)[] = [];
for (var i = 1; i < 100; i++) {
results.push(fizzBuzz(i));
}
window.alert(results);
|
共用型では、1つの値について、“このうちのどれか”を型として表現できます。例えば、上記コードのfizzBuzz
関数では、返ってくる値の型はstring
かnumber
かのどちらかという意味になります。
プリミティブな型のtype guards
// (string | number)[] 型に推論される
// 要素の型が、stringかnumberかどちらか
var array = [100, "str"];
// 1e+2
// STR
// と、表示される
array.forEach((v: string | number) => {
if (typeof v === "string") {
// vの値がstringに絞られる
console.log(v.toUpperCase());
} else {
// vの値が(消去法で)numberに絞られる
console.log(v.toExponential());
}
});
|
プリミティブな型の場合、if
文とtypeof
演算子と===
演算子を使う(=type guardsする)ことで、条件文内での型を指定した型に狭めることができます。else if
を使うことで、型の候補を狭めていき、最後のelse
ではそこまでに登場した型は除外されます。
クラスのインスタンスのtype guards
class Base {
hoge() {
return "hoge";
}
}
class SubA extends Base {
fuga() {
return "fuga";
}
}
class SubB extends Base {
piyo() {
return "piyo";
}
}
var array = [new SubA(), new SubB()];
array.forEach(obj => {
if (obj instanceof SubA) {
console.log(obj.fuga());
} else if (obj instanceof SubB) {
// instanceof によるtype guardsでは毎回ちゃんと絞り込みしないといけない
console.log(obj.piyo());
}
});
|
プリミティブな型ではない場合、instanceof
を使って型の絞り込みを行うことができます。typeof
の場合と違い、最後のelse
で残ったやつ……という書き方ができないところに注意が必要です。
それ以外の場合の対応
var array = ["Hi!", 42];
// プログラマの責任において型アサーションで何とかする!
var v1 = <string>array[0];
var v2 = <number>array[1];
console.log(v1.charAt(0));
console.log(v2.toExponential());
|
typeof
もinstanceof
もうまく使えない場合は、プログラマの責任において型アサーションを使って、単一の型を指定して解決します。なるべくなら、前述の2つのうちのどちらかを使いたいところです。
type alias(型の別名)
// 共用型に名前を付ける
type FooReturns = number | string;
// 頻出表現に名前を付けてメンテナンス時のミスを減らす
interface Foo {
method1(): FooReturns;
method2(): FooReturns;
}
var foo: Foo;
var m1 = foo.method1();
var m2 = foo.method2();
|
type aliasは、型に別名を付けることができます。基本方針として、interface
でできることをtype aliasでやってはいけません。type aliasの出番は、共用型またはタプル型が絡む場合のみでしょう。
TypeScriptとECMAScript 2015(6)、7
TypeScriptでは1.4.1からECMAScript 2015(ECMAScript 6とも呼ばれる。本稿では--target
オプション名にあわせ、以下、ES6と表記する)の要素のサポートを始め、1.5.3でさらにそのサポートを進めています。コンパイル時のターゲットがes3
やes5
か、es6
かで生成されるJavaScriptコードが変化します。そのため最新のES6な書き方をした場合でも、古いブラウザで動作させることができます。
ここからは、TypeScript要素は薄くなりますが、TypeScriptで利用可能なES6な構文を確認していきます。なお、es3
やes5
をターゲットにコンパイルした場合についてはあまり言及しませんので、興味がある方は自分で確かめてみてください。
ChromeやNode.jsなどでは、strict modeのときのみ動作する機能も含まれます。ファイルの先頭か関数の先頭、どちらか適切な方で"use strict";
を宣言するようにしましょう。
- ※ES6はまだ仕様策定がなされたばかりで一般的な日本語訳が定まっていないので、以下ではすべて英語表記で記載します。
let/const
"use strict";
let str = "外側";
console.log(str); // 外側 と表示される
{
// { } でブロックを作るとそこは別世界……
let str = "内側1";
console.log(str); // 内側1 と表示される
}
// ブロックの外に抜けたので内側の定義はなかった事になる
console.log(str); // 外側 と表示される
{
// 変数の値の上書きは普通に外側にも反映される
str = "内側2";
console.log(str); // 内側2 と表示される
}
console.log(str); // 内側2 と表示される
const num = 1;
// コンパイルエラー! numの値を書き換えることはできない
num = 2;
{
// ブロックを作るとそこは別世界なのでOK!
const num = 3;
console.log(num); // 3と表示される
}
|
let
とconst
はJavaScript以外の言語に慣れている人には多分待望の、ブロックスコープの導入です。var
による変数定義と使い方はたいして変わりませんが、{ }
で作るブロックがそのままスコープ(=名前空間の区切れ)になります。
const
を使うと、値の上書きを禁止して定数とすることができます。今後、var
を使う機会はだんだん減っていくことでしょう。
for
とlet
を組み合わせて、次のようなコードも記述できます。
"use strict";
function sample1() {
var i = 100;
// forの中のiは別スコープ
for (let i = 0; i < 5; i++) {
console.log(i);
}
console.log(i);
}
function sample2() {
var i = 100;
// forの中のiは同一スコープ
for (var i = 0; i < 5; i++) {
console.log(i);
}
console.log(i);
}
sample1(); // 0 1 2 3 4 100 と表示される
sample2(); // 0 1 2 3 4 5 と表示される
function sample3() {
for (let i = 0; i < 5; i++) {
console.log(i);
}
// コンパイルエラー!forで定義したiは外側には存在しない!
console.log(i);
}
sample3();
|
template literals
"use strict";
// 書いたものがそのまま文字列になる。改行もできる
let str1 = `例えば
複数行の文字列も
template literalsなら書ける!`;
// ${ } で囲うことで式を埋め込むこともできる
let str2 = `今日は${Math.random() < 0.5 ? "良い" : "悪い"}日だ。`
// 今日は良い日だ。 または 今日は悪い日だ。 と表示される。
console.log(str2);
// template literalsの前に関数名を書くと、決まった形式の呼び出しに変換される(tagged templates)
function tag(strs: TemplateStringsArray, ...values: any[]) {
console.log(strs); // [ 'こんにちは', '・', 'の諸君!' ] と表示される
console.log(values); // [ '少年', '少女' ] と表示される
return "世界はTypeScriptに支配された!";
}
let str3 = tag`こんにちは${"少年"}・${"少女"}の諸君!`;
// 世界はTypeScriptに支配された! と表示される
console.log(str3);
|
template literals(テンプレートリテラル)は、式の埋め込みができるようになった文字列リテラルです。複数行にまたがる文字列の記述も可能です。
式が埋め込めるだけでもなかなかに便利ですが、tagged templates(タグ付きテンプレート)と呼ばれる記法では、指定の関数にtemplate literalsを丸々渡すことができます。実利用の代表例としては、HTMLソースに値を埋め込むときに埋め込む文字列のHTMLエスケープをしたり、i18nなどの多言語化ユーティリティを作成したりなどが考えられます。
shorthand properties
"use strict";
{
let name = "vvakame";
let like = "猫";
// 今までの書き方
let personA = { name: name, like: like };
// 短くかける(personAと同じ意味)
let personB = { name, like };
}
|
shorthand properties(直訳すると「簡略化プロパティ」)は、オブジェクトを作るときの、プロパティ名の宣言と値の指定を一部省略できるというものです。値の指定に使う変数と同名でよい場合は、プロパティ名の記述とその後の:
を省略できます。
destructuring
"use strict";
{
let person = {
name: "vvakame",
like: "猫"
};
// 構造化された値を普通の変数に展開(脱構造化)する!
let { name, like } = person;
// 'vvakame', '猫' と表示される
console.log(name, like);
}
{
let array = [1, 2];
// 配列を普通の変数に展開(脱構造化)する!
let [numA, numB] = array;
// 1 2 と表示される
console.log(numA, numB);
}
// 関数の引数でも使えるよ!
function greeting({name, like }: { name: string, like: string }) {
console.log(`Hi! ${name}. do you want ${like}?`);
}
greeting({
name: "vvakame",
like: "猫"
});
|
destructuringは、オブジェクトや配列を分解(脱構造化)し、値を変数として展開します。de structure ing(=脱・構造・化)の名の通りです。
関数の引数をdestructuringする場合、次のコードに示すように、変数の別名を指定することも可能です。
"use strict";
// {name: namae, like: suki } の : の右側別名の指定。型ではないので注意……(es6の仕様による)
function greeting({name: namae, like: suki }: { name: string, like: string }) {
console.log(`Hi! ${namae}. do you want ${suki}?`);
}
greeting({ name: "vvakame", like: "cat" });
|
spread operator
"use strict";
let array = ["a", "b", "c"];
// concat(array[0], array[1], array[2]); と等価
let str = concat(...array);
// abc と表示される
console.log(str);
// 可変長引数が受け手側でのspread operatorに対応する
function concat(...strs: string[]) {
return strs.join("");
}
|
spread operator(直訳すると「展開演算子」)は、関数を呼ぶときに、配列の中身を展開して呼び出します。今まではFunction
オブジェクトのcall
メソッドやapply
メソッドを駆使しなければ書けなかった呼び出し方を自然に記述できます。
ES6 modules
以下のutils.ts
ファイルとbasis.ts
ファイルの例は、複数ファイルを前提にしているため、Playgroundではなく手元で試した方がよいでしょう。
export default function(word: string): string {
return `Hello! ${word}`;
}
export function bye(word: string): string {
return `Bye, ${word}`;
}
|
// 指定した要素をimportする。as ~ で別名にすることも可能。一番よく使う
import { default as hello, bye } from "./utils";
console.log(hello("TypeScript"));
console.log(bye("JavaScript"));
// default を u2 という名前でimportする
import u2 from "./utils";
console.log(u2("TypeScript"));
// モジュール全体を u3 という名前でimportする。node.js用モジュールに対してよく使うパターン
import * as u3 from "./utils";
console.log(u3.default("TypeScript"));
console.log(u3.bye("JavaScript"));
// 複数の書き方を一度に書く
import u4, { bye as bye4 } from "./utils";
console.log(u4("TypeScript"));
console.log(bye4("JavaScript"));
// ただrequireするだけと等価(副作用のみ必要な場合)
import "./utils";
|
ES6 moduleは、TypeScriptの外部モジュールの仕組みがJavaScriptに公式モジュールとして導入されたものです。TypeScriptにあるimport
とexport
に加え、default
という概念があります。--target es5
オプションと--module commonjs
/amd
オプションをtsc
コマンドに指定することにより、Node.jsやブラウザ上で現在実行可能な形式で出力できます。
default
は、export
した機能のうち、そのモジュール(ファイル)のデフォルトの値です。この例では、関数をdefault
の値にして出力しています。
仕様が固まったとはいえ、今はまだ過渡期であるため、この書き方のサンプルやコードを見ることは少ないと思われます。ES6 module形式でコードを管理していくと今後の発展を先取りできるため、導入の機会があれば積極的に利用するべきです。
なお、export
は次のように記述することもできる。
// importの代わりにexportを書くと別モジュールの内容を再exportできる
export * from "./utils";
export {bye} from "./utils";
let p = {
x: 1,
y: 2,
};
let str = "hi";
// export を後から複数指定することもできる
export { p as default, str};
|
for...of
"use strict";
var array = ["a", "b", "c"];
for (let v of array) {
console.log(v, v.toUpperCase());
}
|
for...of
は繰り返し可能なオブジェクトの要素を反復できます。JavaScript言語仕様のfor...in
との違いは、for...in
がプロパティ名を取るのに対して、for...of
は配列では値を反復するところです。
Array
オブジェクト以外にも、非常に広範囲に利用できるように設計されていますが、TypeScriptにまだgeneratorのサポートが来ていないため、ここでは割愛します。
symbols
"use strict";
module ModuleName {
// p は外部に公開しない
let p = Symbol();
// hi は外部で同一のSymbolを生成可能
let hi = Symbol.for("hi");
export class Sample {
hello(word: string) {
return this[p](word);
}
// 同一のSymbolが手に入らないのでモジュールの外側から呼び出せない! 完全private!
[p](word: string) {
return `Hello, ${word}`;
}
[hi](word: string) {
return `Hi, ${word}`;
}
}
}
let obj = new ModuleName.Sample();
// Hello, TypeScript と表示される
console.log(obj.hello("TypeScript"));
let hi = Symbol.for("hi");
// Hi, TypeScript と表示される
console.log(obj[hi]("TypeScript"));
|
今までに見てきた他の機能は、これまでめんどうくさいコードを書かねば実現できなかったことを、簡単に出来るようにした機能です。symbolsはES6以前の、3や5では完全に同一の仕組みは作れない、本当の新機能です。
Symbol
はその名の通り、“象徴(シンボル)”を作り出します。例えば名前
という“文字列”でプロパティ名を作ってしまうと、名前を知っている人にはアクセスできてしまいます。Symbol
を使えば“象徴”なので、スコープの外側に公開するもしないも、思いのままです。
ES6の言語仕様上でもSymbol
は活用されていて、for...of
文などで垣間見ることができます。
computed properties
"use strict";
let propName = "default";
class Sample {
// 実際のプロパティ名は実行時に算出される
[propName](word: string) {
return `Hello, ${word}`;
}
// 計算式を書いてもよい
["p" + Math.random()]() {
return "ab";
}
}
let objA = new Sample();
// Hello, TypeScript と表示される
console.log(objA[propName]("TypeScript"));
// Hello, 1 と表示される
console.log(objA[propName](1)); // 型を間違えていてもエラーにならない!
propName = "next";
// 実行時エラー! 自分の足を撃ち抜くのは自由だ……
// エラーになるのは、定義済みのプロパティ名まで動的に変わるわけではないので
console.log(objA[propName] ("TypeScript"));
// 死ぬほど運が良ければアクセスできるかも!? (上記の計算式ではランダムな数値になるので)
console.log(objA["p0.111"]);
|
computed propertiesは、頑張って日本語に訳すと“算出プロパティ”とでもいうべきものです。実行時にプロパティ名を計算して決定することができます。
"use strict";
let propName = "default";
// 今まで 先に定義して後から値を詰め込む
let objA: {
[name: string]: string;
} = {};
objA[propName] = "a";
// computed propertiesを使うと定義と一緒に値を詰め込める。
let objB = {
[propName]: "a"
};
|
上記のコードのように、今までより短く書くこともできますが、型チェックが緩くなってしまうため、あまり使わないほうがよいでしょう。
decorators
decorators(デコレータ)は現在、ES7での仕様策定に向けて議論されているものですが、一足早くTypeScriptに実装が入りました。政治的なものを感じなくもないですが、面白い使い方ができるので良しとしましょう。
decoratorsは、Javaでいうアノテーションとほぼ同じ使い方をしますが、仕組み自体は全く違う仕組みによって実現されています。多くのユーザーは自分でdecoratorを作ることはなく、用意されたものを使う場合の方が圧倒的に多いでしょう。
"use strict";
// decoratorsの使用例。decorator自体の定義は末尾にある
@classDecorator
class Sample {
a(): string {
return "";
}
@methodDecorator
b(): string {
return "";
}
@staticMethodDecorator
static c(): string {
return "";
}
@propertyDecorator
d = "";
@accessorDecorator
get e(): string {
return "";
}
f( @parameterDecorator str: string): string {
return `Hello, ${str}`;
}
}
console.log("------");
let obj = new Sample();
console.log(obj.a());
console.log(obj.b());
console.log(Sample.c());
console.log(obj.d);
console.log(obj.e);
console.log(obj.f("parameter"));
// decoratorの定義
function classDecorator(sampleClazz: typeof Sample): typeof Sample {
console.log("classDecorator", arguments);
sampleClazz.prototype.a = function() {
return "Hello from classDecorator!";
}
return null; // 値を返すとその値で置き換えることができる
}
function methodDecorator(prototypeOfSample: any, key: string, propertyDescription: PropertyDescriptor): PropertyDescriptor {
console.log("methodDecorator", arguments);
return null; // 値を返すとその値で置き換えることができる
}
function staticMethodDecorator(sampleClazz: typeof Sample, key: string, propertyDescription: PropertyDescriptor): PropertyDescriptor {
console.log("staticMethodDecorator", arguments);
return null; // 値を返すとその値で置き換えることができる
}
function propertyDecorator(prototypeOfSample: any, key: string): void {
console.log("propertyDecorator", arguments);
}
function accessorDecorator(prototypeOfSample: any, key: string, propertyDescription: PropertyDescriptor): PropertyDescriptor {
console.log("accessorDecorator", arguments);
return null; // 値を返すとその値で置き換えることができる
}
function parameterDecorator(prototypeOfSample: any, methodName: string, parameterIndex: number): void {
console.log("parameterDecorator", arguments);
// DIや呼び出し時に実行を行いたい場合はメソッドの差し込みを自力で実装する必要がありそう
}
|
使い方の基本は、装飾(decorate)したい要素の前に@decorator名
と書くことです。decoratorの実装は、指定されたプロパティの要素を置き換え、処理に介入するチャンスを得ます。tsc
コマンドでコンパイルするときは、--experimentalDecorators
オプションの指定が必要になります。現在のECMAScriptの仕様では、実行時にリフレクションなどを用いてメタデータを取得するAPIは定義されていません*3。そのため、このようにdecorator側に多くの裁量を与える形式にしたのでしょう。
- *3 TypeScript 1.5.3では、
--emitDecoratorMetadata
オプションをコンパイル時に与えることにより、実行時にTypeScriptコード上での型情報についてアクセスさせられるようになります。