TypeScript/handbook抄訳② ModulesからGenericsまで
前回に引続きTypeScriptの公式サイトにあるhandbookページのコンテンツを抄訳しました(2016/03/16取得)。訳の正確性については期待しないでください。文法に関する説明にはなるべく書かれている通りとなるよう注意を払っているつもりですが、それ以外の箇所については割愛や乱暴な意訳をしているところがたくさんあります。
今回はModulesからGenericsまでを掲載します。一旦これで終わりにします。
* * *
モジュール
ここではモジュールを使ってTypeScriptコードを整理する方法─内部モジュールと外部モジュールのそれぞれがどのような場所で使うのが適切なのか、そしてそれは実際どのように使うのか─を示そう。それからまた外部モジュールのより詳細な情報、そしてしばしば遭遇するであろう落とし穴についても見ていくことになろう。
最初のステップ
まずこのセクションを通じて見ていくことになるプログラム・コードを示す。とてもシンプルな文字列バリデータだ。このバリデータを使えばフォームの入力内容や外部からもたらされたデータ・ファイルのフォーマットを検証することができる。
単一ファイルに記述されたバリデータ
nterface StringValidator { isAcceptable(s: string): boolean; } var lettersRegexp = /^[A-Za-z]+$/; var numberRegexp = /^[0-9]+$/; class LettersOnlyValidator implements StringValidator { isAcceptable(s: string) { return lettersRegexp.test(s); } } class ZipCodeValidator implements StringValidator { isAcceptable(s: string) { return s.length === 5 && numberRegexp.test(s); } } // バリデータ動作確認のためのサンプル・データ var strings = ['Hello', '98052', '101']; // 使用するバリデータの準備 var validators: { [s: string]: StringValidator; } = {}; validators['ZIP code'] = new ZipCodeValidator(); validators['Letters only'] = new LettersOnlyValidator(); // 各文字列がバリデーションをパスするかどうかを表示する strings.forEach(s => { for (var name in validators) { console.log('"' + s + '" ' + (validators[name].isAcceptable(s) ? ' matches ' : ' does not match ') + name); } });
モジュラー性を加える
バリデータの数が増えるにしたがって、自分たちでつくった型を把握し、他のオブジェクトと名前衝突を起こす心配のないように何かしらのスキーマのようなものが欲しくなる。このような場合はグローバルな名前空間上で一意性のある名前を考案するのではなく、モジュールを利用しよう。
次の例ではバリデータとそれに関係する型はValidation
というモジュールの中に移動している。インターフェースやクラスはモジュールの外部に公開したいので、それらの定義の前にはexport
キーワードが付与されている。一方でettersRegexp
とnumberRegexp
の2つの変数は実装の詳細に該当し、外部からは参照させたくないから、export
キーワードが付与されていない。エクスポートされた型はモジュール名の修飾付きで外部から参照できる。
モジュール化されたバリデータ
module Validation { export interface StringValidator { isAcceptable(s: string): boolean; } var lettersRegexp = /^[A-Za-z]+$/; var numberRegexp = /^[0-9]+$/; export class LettersOnlyValidator implements StringValidator { isAcceptable(s: string) { return lettersRegexp.test(s); } } export class ZipCodeValidator implements StringValidator { isAcceptable(s: string) { return s.length === 5 && numberRegexp.test(s); } } } // バリデータ動作確認のためのサンプル・データ var strings = ['Hello', '98052', '101']; // 使用するバリデータの準備 var validators: { [s: string]: Validation.StringValidator; } = {}; validators['ZIP code'] = new Validation.ZipCodeValidator(); validators['Letters only'] = new Validation.LettersOnlyValidator(); // 各文字列がバリデーションをパスするかどうかを表示する strings.forEach(s => { for (var name in validators) { console.log('"' + s + '" ' + (validators[name].isAcceptable(s) ? ' matches ' : ' does not match ') + name); } });
複数のファイルに分割する
アプリケーションが成長して大きくなると、メンテナンスを容易にするためファイルを分割する必要が出てくる。
次のコードでファイルは分割されているにもかかわらず、Validation
モジュールは引続き同じ一体のモジュールとして振る舞う。
ファイルの先頭にはreference
タグが追加され、コンパイラに対してファイルの依存関係が示されている。
複数ファイルで定義した内部モジュール
Validation.ts
module Validation { export interface StringValidator { isAcceptable(s: string): boolean; } }
LettersOnlyValidator.ts
/// <reference path="Validation.ts" /> module Validation { var lettersRegexp = /^[A-Za-z]+$/; export class LettersOnlyValidator implements StringValidator { isAcceptable(s: string) { return lettersRegexp.test(s); } } }
ZipCodeValidator.ts
/// <reference path="Validation.ts" /> module Validation { var numberRegexp = /^[0-9]+$/; export class ZipCodeValidator implements StringValidator { isAcceptable(s: string) { return s.length === 5 && numberRegexp.test(s); } } }
Test.ts
/// <reference path="Validation.ts" /> /// <reference path="LettersOnlyValidator.ts" /> /// <reference path="ZipCodeValidator.ts" /> // バリデータ動作確認のためのサンプル・データ var strings = ['Hello', '98052', '101']; // 使用するバリデータの準備 var validators: { [s: string]: Validation.StringValidator; } = {}; validators['ZIP code'] = new Validation.ZipCodeValidator(); validators['Letters only'] = new Validation.LettersOnlyValidator(); // 各文字列がバリデーションをパスするかどうかを表示する strings.forEach(s => { for (var name in validators) { console.log('"' + s + '" ' + (validators[name].isAcceptable(s) ? ' matches ' : ' does not match ') + name); } });
ファイルを複数に分割すると、すべてのコンパイル済みコードが読み込まれるように保証する必要が出てくる。そのための方法は2つある。
1つ目。コンパイル時にtscコマンドの--out
フラグを使えば入力ファイルはすべて1つのJavaScript出力ファイルに結合される:
tsc --out sample.js Test.ts
コンパイラはreference
タグに基づいて自動的にコードを連結する。もちろん個々のファイルを明示的に指定することも可能だ:
tsc --out sample.js Validation.ts LettersOnlyValidator.ts ZipCodeValidator.ts Test.ts
2つ目。tsファイルごとにjsファイルを作成し(これがデフォルトの動作だ)、Webページのscript
タグでそれらを個別に、しかも適切な順序でロードする:
<script src="Validation.js" type="text/javascript" /> <script src="LettersOnlyValidator.js" type="text/javascript" /> <script src="ZipCodeValidator.js" type="text/javascript" /> <script src="Test.js" type="text/javascript" />
外部化する
TypeScriptには外部モジュールという概念もある。外部モジュールは2つのケースで利用される:node.js と require.jsである。アプリケーションがこのいずれも利用していないなら外部モジュールを利用する必要はない。すでに解説した内部モジュールの仕組みを使うのがベストだ。
外部モジュールではファイル間の関係性はファイル単位のインポート/エクスポートによって示される。TypeScriptでは何であれトップレベルにimport
/export
キーワードが含まれるファイルは外部モジュールと見なされる。
下記のコードでは外部モジュールの仕組みが利用されているが、ここにはもはやmodule
キーワードが登場しないことに注意されたい。ファイルそのものがモジュールを構成しており、モジュールはファイル名により識別されるのである。
reference
タグはimport
文で置換えられている。import
文は2つの部分に分かれている:このファイルの中でモジュールを指すのに使用される名前と、そのモジュールを指すパスを伴うrequire
キーワードである:
import someMod = require('someModule');
モジュールの外部からアクセスできるオブジェクトを示すにはexport
キーワードを使う。内部モジュールと異なるのはそれがトップレベルの宣言として使用されることだ。
コンパイルの際はコマンドライン上で作成するモジュールのターゲットを指定する必要がある。node.jsの場合--module commonjs
であり、require.jsの場合は--module amd
を指定する。例えば:
tsc --module commonjs Test.ts
コンパイルすると、それぞれの外部モジュールは独立した.jsファイルになる。reference
タグの時と同じようにコンパイラはimport
文にしたがってモジュールに依存するファイルのコンパイルを行ってくれる。
Validation.ts
export interface StringValidator { isAcceptable(s: string): boolean; }
LettersOnlyValidator.ts
import validation = require('./Validation'); var lettersRegexp = /^[A-Za-z]+$/; export class LettersOnlyValidator implements validation.StringValidator { isAcceptable(s: string) { return lettersRegexp.test(s); } }
ZipCodeValidator.ts
import validation = require('./Validation'); var numberRegexp = /^[0-9]+$/; export class ZipCodeValidator implements validation.StringValidator { isAcceptable(s: string) { return s.length === 5 && numberRegexp.test(s); } }
Test.ts
import validation = require('./Validation'); import zip = require('./ZipCodeValidator'); import letters = require('./LettersOnlyValidator'); // バリデータ動作確認のためのサンプル・データ var strings = ['Hello', '98052', '101']; // 使用するバリデータの準備 var validators: { [s: string]: validation.StringValidator; } = {}; validators['ZIP code'] = new zip.ZipCodeValidator(); validators['Letters only'] = new letters.LettersOnlyValidator(); // 各文字列がバリデーションをパスするかどうかを表示する strings.forEach(s => { for (var name in validators) { console.log('"' + s + '" ' + (validators[name].isAcceptable(s) ? ' matches ' : ' does not match ') + name); } });
export =
直前の例で使用されたバリデータは、各モジュールからエクスポートされている唯一の値だった。このような場合、いちいちその値をモジュール名の修飾付きで参照せねばならないのは面倒である。
export =
構文を使うとモジュールから単一のオブジェクトのみをエクスポートすることができる。それはクラス、インターフェース、モジュール、関数、あるいは列挙型でも何でも構わない。それをインポートすると、インポートに利用した名前のまま、修飾なしに利用できる。
次の例では各外部モジュールがexport =
構文を使って単一のValidator
実装をエクスポートするように変更されている。モジュールのAPIを利用する側のコードもシンプルになった。
Validation.ts
export interface StringValidator { isAcceptable(s: string): boolean; }
LettersOnlyValidator.ts
import validation = require('./Validation'); var lettersRegexp = /^[A-Za-z]+$/; class LettersOnlyValidator implements validation.StringValidator { isAcceptable(s: string) { return lettersRegexp.test(s); } } export = LettersOnlyValidator;
ZipCodeValidator.ts
import validation = require('./Validation'); var numberRegexp = /^[0-9]+$/; class ZipCodeValidator implements validation.StringValidator { isAcceptable(s: string) { return s.length === 5 && numberRegexp.test(s); } } export = ZipCodeValidator;
Test.ts
import validation = require('./Validation'); import zipValidator = require('./ZipCodeValidator'); import lettersValidator = require('./LettersOnlyValidator'); // バリデータ動作確認のためのサンプル・データ var strings = ['Hello', '98052', '101']; // 使用するバリデータの準備 var validators: { [s: string]: validation.StringValidator; } = {}; validators['ZIP code'] = new zipValidator(); validators['Letters only'] = new lettersValidator(); // 各文字列がバリデーションをパスするかどうかを表示する strings.forEach(s => { for (var name in validators) { console.log('"' + s + '" ' + (validators[name].isAcceptable(s) ? ' matches ' : ' does not match ') + name); } });
エイリアス
モジュールの利用をシンプルにするもう1つの方法はimport q = x.y.z
構文でよく使用するオブジェクトにより短い名前を付与するというものだ。import x = require('name')
構文と勘違いしないで欲しい。それは外部モジュールのロードに使用するものだ。一方、新しく紹介した構文は単に指定されたシンボルに対してエイリアスを付与するものだ。この種類のインポート(一般にエイリアスとして知られている)は外部モジュールで定義されたオブジェクトも含め、あらゆる識別子に対して使用することができる。
他のJavaScriptライブラリを使用する
JavaScriptで書かれたライブラリの「かたち」を描き出す(インターフェースを定義する)には、ライブラリが公開しているAPIを宣言してやる必要がある。JavaScriptライブラリはふつう2・3のトップレベルのオブジェクトのみを外部に公開しているから、それらを表すのにモジュールの概念を使うのは都合が良い。TypeScriptでは実装を持たない宣言を「アンビエント」と呼ぶ。典型的にはそれらは.d.tsファイルで宣言される。もしC/C++に親しんだプログラマであれば、.hファイルやexternの類だと考えればよいだろう。それではまず内部モジュールを使用した例を見てみよう:
D3.d.ts (抜粋)
declare module D3 { export interface Selectors { select: { (selector: string): Selection; (element: EventTarget): Selection; }; } export interface Event { x: number; y: number; } export interface Base extends Selectors { event: Event; } } declare var d3: D3.Base;
アンビエント外部モジュール
node.jsでは多くのタスクが1つもしくは複数のモジュールをロードすることにより実現している。これらのモジュールの「アンビエント」を1つ1つ個別の.d.tsファイルに定義することも可能だが、大きな単一のファイルでまとめて記述してしまうほうがより便利であろう。そのためにはまずモジュール名を引用符で囲おう:
node.d.ts (抜粋)
declare module "url" { export interface Url { protocol?: string; hostname?: string; pathname?: string; } export function parse(urlStr: string, parseQueryString?, slashesDenoteHost?): Url; } declare module "path" { export function normalize(p: string): string; export function join(...paths: any[]): string; export var sep: string; }
そうして宣言したモジュールを/// <reference>
記法でロードする─この方法はimport url = require('url');
記法に似ている:
///<reference path="node.d.ts"/> import url = require("url"); var myUrl = url.parse("http://www.typescriptlang.org");
モジュールの落とし穴
ここでは内部モジュール/外部モジュールの仕組みを使用する上でしばしば遭遇する落とし穴とその回避の仕方を紹介しよう。
/// <reference>を外部モジュールに対して使う
よくある間違いの1つは単一の外部モジュールを参照するのにimport
ではなく/// <reference>
を用いるケースである。この2つの違いを理解するにはまずコンパイラが外部モジュールの型情報を見つけるための3つの方法について理解する必要がある:
第1にimport x = require(...);
宣言で指定された名前に基づき.tsファイルを検索する。このファイルは実装を含み、トップレベルのインポートもしくはエクスポートを行うものでなければならない。
第2に.d.ts ファイルを検索する。第1のものと同様だが、実装を含まない、宣言ファイルが対象となる(トップレベルのインポート/エクスポートを伴っていても良い)。
第3に「アンビエント外部モジュール」のセクションで見たように、アンビエントの宣言(これ自体は外部モジュールではなく、あくまでもそれを利用するための宣言である)のうちからマッチするものを検索する。
myModules.d.ts
// a .d.tsファイルや.tsファイルの中。外部モジュールではないどこか: declare module "SomeModule" { export function fn(): string; }
myOtherModule.ts
/// <reference path="myModules.d.ts" /> import m = require("SomeModule");
reference
タグは宣言ファイル─外部モジュールのためのアンビエントが記述された─の場所を指している。
不要な名前空間
例えば、内部モジュールを外部モジュールに切り替えた場合、結果としてファイルが次のような感じになることがあり得る:
shapes.ts
export module Shapes { export class Triangle { /* ... */ } export class Square { /* ... */ } }
トップレベルのモジュールShapes
がTriangle
とSquare
を包み込んでいるが、これには何の意味もない。こんなことをするとモジュールの利用者は混乱に頭を悩ませられてしまう:
shapeConsumer.ts
import shapes = require('./shapes'); var t = new shapes.Shapes.Triangle(); // shapes.Shapes?
TypeScriptにおける外部モジュールの重要な特徴の1つは、2つの異なる外部モジュールは同じスコープで同じ名前を利用することが決してできないということである。モジュールをいかなる名前に割当てるかは利用者が決定し、その名前に重複はあり得ない(重複した名前を使おうとすればコンパイル・エラーになる)ので、現にエクスポートしているシンボルをあえて名前空間で包み込んでやる必要はない。
繰り返しになるが、なぜ外部モジュールを名前空間で包むべきでないかというと、名前空間の一般的な概念はプログラム/APIの構成部品のため論理的なグループを提供すること、そして名前空間の衝突を防止することでだからである。外部モジュールファイルはそれ自体がもうすでに1つの論理的グループで、トップレベルの名前はそれをインポートする側で決めるものなので、エクスポートしたいオブジェクトのためにそれを包むモジュールを用意する必要はないのである。
外部モジュールにまつわるトレードオフ
ちょうどJavaScriptファイルとモジュールとが1対1に対応しているように、TypeScriptも外部モジュールとそこから生成されたJavaScriptコードとが1対1に対応している。結果として、--out
コンパイラ・スイッチを使って複数の外部モジュールを1つのJavaScriptファイルに結合するということは不可能となっている。
関数
関数はJavaScriptにおいてアプリケーションを構築する際の基本的な構成要素となっている。抽象化を行い、クラスを模倣し、情報を秘匿し、モジュール化する─そのすべてに関数が関わってくる。TypeScriptにはクラスとモジュールが独立した構成要素として存在するが、それでもものごとをどのように実現するか決定するという点で依然として関数は重要な役割を演じ続ける。TypeScriptでは関数の生成に関してJavaScriptにおける関数にはない新たな機能も追加されている。
JavaScript/TypeScriptの関数
はじめに、JavaScriptにおけるのと同様、TypeScriptの関数は名前付き関数としても匿名関数としても定義することができる。開発中のアプリケーションに対していずれかより適切なアプローチを選ぶのは開発者の役割である─APIのなかで一連の名前付き関数を宣言するのでもよいし、1回限り利用される匿名関数を他の関数に手渡すようにして実装するのでもよい。
JavaScriptにおけるのと同様、関数は関数本体の外側の変数(関数が定義されたときのスコープチェーン上にあった変数)に対して値の参照/設定をすることができる。これを変数の「キャプチャ」と呼ぶ。それがどのようにはたらくか、そしてこのテクニックを用いることによるトレードオフは何かという点は、この記事のスコープ外である。そうであるにしても、この仕組みについてきちんと理解しておくことはJavaScript/TypeScriptを使って開発をしていく上で重要である(つまり次のコードがのどのようにはたらくか、きちんと理解できていないといけない)。
var z = 100; function addToZ(x, y) { return x+y+z; }
関数の型
上述のサンプルに型情報を付与してみよう:
function add(x: number, y: number): number { return x+y; } var myAdd = function(x: number, y: number): number { return x+y; };
仮引数のそれぞれと、戻り値について型情報を指定できる。TypeScriptでは戻り値型は関数本体のreturn
文の様態から推論させることができる。したがって多くの場合この戻り値型の宣言は省略可能である。
関数の型を記述する
それでは関数の型を記述してみよう。
var myAdd: (x:number, y:number)=>number = function(x: number, y: number): number { return x+y; };
関数の型は関数そのものの宣言同様に2つの部品からなる:引数の型と戻り値の型だ。関数の型を記述する場合はそのいずれもが必須となる。第1の部品である引数リストについては、個々の引数の名前を工夫することで可読性を高めることもできる:
var myAdd: (baseValue:number, increment:number)=>number = function(x: number, y: number): number { return x+y; };
引数の型は一致している必要があるが、名前はその限りではない。
第2の部品は戻り値型である。戻り値型はアロー(=>
)の後ろに記述する。前述のとおりこの戻り値型の記述は省略不可能であり、もし関数が戻り値を持たないのであればvoidを記述する。
注目すべき点としては、関数の型を構成するのは引数の型と戻り値の型だけであり、先だって論じた「キャプチャ」される変数の型は関わってこないことである。実際のところ「キャプチャ」された変数は「隠された状態」の一部であり、APIがそれを外部に曝け出すことはないのである。
型推論
すでにお気付きの方もいるかも知れないが、TypeScriptコンパイラは関数の型と関数そのものの宣言のいずれか一方にのみ型情報が指定されていればもう一方のそれは指定なしでも推定できる:
// myAddの型をコンパイラは推論できる var myAdd = function(x: number, y: number): number { return x+y; }; // コンパイラには'x'と'y'がnumber型であることがわかる var myAdd: (baseValue:number, increment:number)=>number = function(x, y) { return x+y; };
これは「文脈型付け」と呼ばれる型推論の一形態である。この機能のおかげでプログラムの型安全性を維持しつつコーディング量を抑えることが可能になる。
オプションの引数とデフォルトの引数
JavaScriptと違いTypeScriptでは関数の引数はいずれも指定が必須である。もちろん引数として渡す値としてnull
を指定することはできる。しかし何であれ関数呼び出しの箇所で過不足なく、期待された数・型の引数の指定がなされていることがコンパイラによりチェックされる。
function buildName(firstName: string, lastName: string) { return firstName + " " + lastName; } var result1 = buildName("Bob"); //エラー。引数が少なすぎる。 var result2 = buildName("Bob", "Adams", "Sr."); //エラー。今度は多すぎる。 var result3 = buildName("Bob", "Adams"); // OK。ぴったりだ。
JavaScriptではいずれの仮引数もオプションのものと見なされている。そして関数呼び出しでは実際に引数を省略することが可能である。省略された引数に対応する仮引数にはundefined
が設定される。TypeScriptで同じことをしたい場合はその仮引数名の後ろに「?
」を付ける:
function buildName(firstName: string, lastName?: string) { if (lastName) return firstName + " " + lastName; else return firstName; } var result1 = buildName("Bob"); //今度はエラーにならずちゃんと動く。 var result2 = buildName("Bob", "Adams", "Sr."); //エラー。多すぎる。 var result3 = buildName("Bob", "Adams"); //OK。ぴったりだ。
オプションの仮引数は必須の仮引数の後ろ(右側)にしか置くことはできない。
オプションの仮引数が関数呼び出し時に指定されなかったときデフォルト値として設定させたい値をあらかじめ指定しておくこともできる。これはデフォルトの仮引数と呼ばれる。仮引数名の後ろには「?
」ではなく「=
」とデフォルト値を記述する:
function buildName(firstName: string, lastName = "Smith") { return firstName + " " + lastName; }
デフォルトの仮引数も必須の仮引数の後ろ(右側)にしか置くことはできない。
デフォルト値があろうとなかろうとオプションの仮引数には違いがないので、関数の型としては(firstName: string, lastName?: string)=>string
になる。
レスト引数(可変長引数)
必須の仮引数も、オプションの仮引数も、デフォルトの仮引数も、その1つ1つが関数呼び出し時に指定される引数の1つ1つに対応する点では変わりない。しかし時として関数に一連の複数の値を一度に渡したいとか、値の個数が宣言時には定まらないということがある。JavaScriptであればあらゆる関数の内部において暗黙のうちに割当てられるarguments
変数を利用するところである。
TypeScriptではこれらの可変長の引数を明示的に宣言した配列型の仮引数に割り当てることができる:
function buildName(firstName: string, ...restOfName: string[]) { return firstName + " " + restOfName.join(" "); } var employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
この仮引数をレスト仮引数(rest parameter)と呼ぶ。レスト仮引数は「...
」に続けて名前を記述する。この記述はオプションの仮引数と同様、関数の型情報にも反映される:
function buildName(firstName: string, ...restOfName: string[]) { return firstName + " " + restOfName.join(" "); } var buildNameFun: (fname: string, ...rest: string[])=>string = buildName;
ラムダ構文と「this」の使用
JavaScriptの関数の中で「this
」がどのようにはたらくかは、この言語で開発をするプログラマにとって共通のテーマである。実際「this
」の使用方法について学ぶことは開発者がJavaScriptというプログラミング言語に順応していくための一種の通過儀礼のようなものになっている。TypeScriptはJavaScriptのスーパーセットなので、TypeScriptのプログラマにとっても事情は変わらない。
「this
」は関数が呼び出された時に暗黙に割当てられる変数である。これは非常に強力/柔軟な特徴を持つのだが、結果として関数が呼び出される文脈について常に考慮する必要が生じる。とりわけ関数がコールバックとして利用されている場合、悪名高い問題を引き起こすのだ:
var deck = { suits: ["hearts", "spades", "clubs", "diamonds"], cards: Array(52), createCardPicker: function() { return function() { var pickedCard = Math.floor(Math.random() * 52); var pickedSuit = Math.floor(pickedCard / 13); return {suit: this.suits[pickedSuit], card: pickedCard % 13}; } } } var cardPicker = deck.createCardPicker(); var pickedCard = cardPicker(); alert("card: " + pickedCard.card + " of " + pickedCard.suit);
このサンプル・コードを実行すると、期待されたアラート表示ではなく実行時エラーに遭遇することだろう。原因はcreateCardPicker
により生成された関数の定義で利用されている「this
」に「deck
」ではなくオブジェクトではなく「window
」オブジェクトが設定されるからである(ただしstrict
モードで実行する場合はundefined
が設定される)。
この問題を解消する方法の1つは、function
式をラムダ構文(()=>{}
)に切り替えることである。ラムダ構文における「this
」はその関数が定義されたスコープにおける「this
」を参照する:
var deck = { suits: ["hearts", "spades", "clubs", "diamonds"], cards: Array(52), createCardPicker: function() { // Notice: the line below is now a lambda, allowing us to capture 'this' earlier return () => { var pickedCard = Math.floor(Math.random() * 52); var pickedSuit = Math.floor(pickedCard / 13); return {suit: this.suits[pickedSuit], card: pickedCard % 13}; } } } var cardPicker = deck.createCardPicker(); var pickedCard = cardPicker(); alert("card: " + pickedCard.card + " of " + pickedCard.suit);
「this
」についてより詳しく理解するには、Yahuda Katz氏の「JavaScript関数の実行とthisについて理解する」を参照のこと。
多重定義(オーバーロード)
JavaScriptは本質的に非常に動的な言語である。1つの関数が渡された引数の形態に対応する複数の型のオブジェクトを返すなどということは珍しいことでも何でもない。
var suits = ["hearts", "spades", "clubs", "diamonds"]; function pickCard(x): any { // Check to see if we're working with an object/array // if so, they gave us the deck and we'll pick the card if (typeof x == "object") { var pickedCard = Math.floor(Math.random() * x.length); return pickedCard; } // Otherwise just let them pick the card else if (typeof x == "number") { var pickedSuit = Math.floor(x / 13); return { suit: suits[pickedSuit], card: x % 13 }; } } var myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }]; var pickedCard1 = myDeck[pickCard(myDeck)]; alert("card: " + pickedCard1.card + " of " + pickedCard1.suit); var pickedCard2 = pickCard(15); alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
このような挙動を型安全な世界で表現するには多重定義を用いる:
var suits = ["hearts", "spades", "clubs", "diamonds"]; function pickCard(x: {suit: string; card: number; }[]): number; function pickCard(x: number): {suit: string; card: number; }; function pickCard(x): any { // Check to see if we're working with an object/array // if so, they gave us the deck and we'll pick the card if (typeof x == "object") { var pickedCard = Math.floor(Math.random() * x.length); return pickedCard; } // Otherwise just let them pick the card else if (typeof x == "number") { var pickedSuit = Math.floor(x / 13); return { suit: suits[pickedSuit], card: x % 13 }; } } var myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }]; var pickedCard1 = myDeck[pickCard(myDeck)]; alert("card: " + pickedCard1.card + " of " + pickedCard1.suit); var pickedCard2 = pickCard(15); alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
「function pickCard(x): any
」は多重定義リストには含まれない。多重定義リストに含まれるのは先に定義されている2つだけである。3つ目のそれは多重定義リストで宣言された関数の型に対応する実装を提供するものであり、仮にpickCard
関数に上の2つの宣言で規定されたのと異なる型・数の引数を指定した場合、コンパイル・エラーとなる。
ジェネリクス
ここではジェネリクスの説明のため恒等関数の例を示そう。まずジェネリックを使わない場合、恒等関数は2通りの実装となる─つまり、特定の型を指定されたものか:
function identity(arg: number): number { return arg; }
さもなくば「any」型を指定されたものか:
function identity(arg: any): any { return arg; }
「any
」を使用したパターンはある意味「ジェネリック」だが、そこからはもう型安全性は失われている。関数呼び出し時に数値型の引数を渡したところで、戻り値型はやはり「any
」となってしまう。
ジェネリクスを使う場合、引数の型を元にしてそれを戻り値の型として利用することができる。このとき型変数を利用することになる─これは文字通り値ではなく値の型を保持する変数である:
function identity<T>(arg: T): T { return arg; }
この例では型仮変数「T
」が導入されている。関数のユーザが引数として渡した実際の値に応じて、その値の方が「T
」に置換えられる。その同じ型情報が戻り値にも適用される。これで入力の型も出力の型も明確になった。関数は「ジェネリック」になり、しかも引続き型安全性を保持している。
ジェネリクスを使った関数を呼び出す方法には2種類がある─型引数を設定するか:
var output = identity<string>("myString"); // type of output will be 'string'
「型引数推論」を利用する:
var output = identity("myString"); // type of output will be 'string'
コンパイラは引数の値の型から型仮引数を推論し、それを戻り値にも利用する。型仮引数推論によりコードは簡潔なままに保たれ、可読性が向上する。型引数の明示的な指定が必要になるのは、より複雑なケースではコンパイラが型仮引数の推論をしきれないときである。
C#やJava同様に型引数は(通常の)引数のジェネリック型の型引数にも適用される:
function loggingIdentity<T>(arg: T[]): T[] { console.log(arg.length); // Array has a .length, so no more error return arg; }
配列型には2つの記法があるので次の記述法も有効である:
function loggingIdentity<T>(arg: Array<T>): Array<T> { console.log(arg.length); // Array has a .length, so no more error return arg; }
ジェネリック型
ジェネリック関数の型は型仮引数リストが先頭に付く以外は非ジェネリックの関数と同様で、関数定義に似ている:
function identity<T>(arg: T): T { return arg; } var myIdentity: <T>(arg: T)=>T = identity;
型仮引数の名前が違っても型としての同一性は変わらない。重要なのは型仮引数の数とその使われ方である:
function identity<T>(arg: T): T { return arg; } var myIdentity: <U>(arg: U)=>U = identity;
オブジェクト・リテラルの呼び出しシグネチャの形式でも記述ができる:
function identity<T>(arg: T): T { return arg; } var myIdentity: {<T>(arg: T): T} = identity;
この記法からジェネリック・インターフェースの記法が導かれる。呼び出しシグネチャをオブジェクト・リテラルの中からインターフェース宣言の中に移動する:
interface GenericIdentityFn { <T>(arg: T): T; } function identity<T>(arg: T): T { return arg; } var myIdentity: GenericIdentityFn = identity;
これに似ているが、ジェネリックの型仮引数はインターフェース全体の型仮引数の位置へと移動することもできる。このようにすると型仮引数はインターフェースの他のすべてのメンバーの宣言で参照可能になる。
interface GenericIdentityFn<T> { (arg: T): T; } function identity<T>(arg: T): T { return arg; } var myIdentity: GenericIdentityFn<number> = identity;
しかしちょっとした変化が起きていることに注意してほしい。ジェネリック関数の説明と言っておきながら、最後の例ではジェネリック型の一部である非ジェネリックな関数が登場している。GenericIdentityFn
を使用するとき、まずは対応する型引数(例ではnumber
型)を指定する必要があり、これにより関数の呼び出しシグネチャも確定する。
次に見ていくように、ジェネリック・インターフェース同様、ジェネリック・クラスも当然作成できる。ただしジェネリックなモジュールや列挙型は定義できない。
ジェネリック・クラス
ジェネリック・クラスはジェネリック・インターフェースと似たかたちをしている。ジェネリック・クラスはクラス名の後ろにジェネリック型仮引数をとる。
class GenericNumber<T> { zeroValue: T; add: (x: T, y: T) => T; } var myGenericNumber = new GenericNumber<number>(); myGenericNumber.zeroValue = 0; myGenericNumber.add = function(x, y) { return x + y; };
ところでこの例では型引数がnumber
でなくてはならないという制約はどこにもない。したがって型引数としてstring
やもっと複雑なオブジェクトの型を指定することもできる。またインターフェースの場合と同様型仮引数はクラスのメンバーすべてから参照可能である。一方この型仮引数はクラスのインスタンスの側からしか参照できない。したがってクラスのstatic
な側からは参照できない。
ジェネリック制約
完全に任意の型というのではなく、特定の名前と型のプロパティを持つ型だけを対象としたいということがある。具体的な型がどんな型であっても構わないがただし最低限特定のメンバーを持っている型だけを受け入れたいということだ。このような場合、その要件を表現するインターフェースとextends
キーワードを使って型仮引数に制約を設けることができる:
interface Lengthwise { length: number; } function loggingIdentity<T extends Lengthwise>(arg: T): T { console.log(arg.length); // OK。lengthプロパティの存在は保証されている return arg; }
こうして制約を設けるともはやいかなる型でも受け入れ可能というわけではなくなり:
loggingIdentity(3); // エラー:number型はlengthプロパティを持たない
所定のプロパティを持つ型の値である場合だけ受け入れ可能ということになる:
loggingIdentity({length: 10, value: 3});
ジェネリック制約の中での型仮引数の使用
時として他の型仮引数に制約付けられた型仮引数が便利なときもある。しかし制約の中で型仮引数を使用することはできない:
function find<T, U extends Findable<T>> (n: T, s: U) { // エラー:型仮引数が制約の中で使用されている // ... } find (giraffe, myAnimals);
このような場合は型仮引数を制約で置き換えよう:
function find<T>(n: T, s: Findable<T>) { // ... } find(giraffe, myAnimals);
ジェネリクスの中でのクラス型の利用
TypeScriptで、ジェネリクスを使用したファクトリ・メソッドを定義するとき、ファクトリが生成するオブジェクトのコンストラクタ関数を参照する必要が出てくる。それには例えば次のようにする:
function create<T>(c: {new(): T; }): T { return new c(); }
さらに、より高度な例として、コンストラクタ関数とそのクラスのインスタンスの側との間の関係を推論し制約付けるためprototype
プロパティを使用することもできる:
class BeeKeeper { hasMask: boolean; } class ZooKeeper { nametag: string; } class Animal { numLegs: number; } class Bee extends Animal { keeper: BeeKeeper; } class Lion extends Animal { keeper: ZooKeeper; } function findKeeper<A extends Animal, K> (a: {new(): A; prototype: {keeper: K}}): K { return a.prototype.keeper; } findKeeper(Lion).nametag; // 型チェックが有効!
* * *
原典はTypeScriptの公式サイトにあるhandbookページ(2016/03/16取得)。