Closure CompilerでタイプセーフJavaScriptコーディング
Closure Compilerを使用したタイプセーフJavaScriptコーディングについてまとめ。
- Closure CompilerはClosure Toolsの一部で、
- JavaScriptコードを解析して圧縮と最適化を行うJavaScriptToJavaScriptコンパイラです。
- 最適化だけでなく、シンタックスや型のチェック機能も提供。
- 型チェックは、JsDocコメントの形式で記載された型情報をもとに行われます。
- 型システムは、ECMAScript4の仕様に準拠している模様。
- Closure Compilerを使用することで、ECMAScript3の世界でECMAScript4ライクな型システムが使えます。(型情報をコメントに書くので、スマートさには欠けますが・・)
Closure Compilerのインストール
- Closure Compilerのサイトよりzipアーカイブを取得して展開すればOK。
- Javaプログラムなので実行にはJREも必要です。インストールされていない場合は別途取得してインストールしておきます。
基本的な使い方
java -jar compiler.jar --charset <jsファイルのエンコード:デフォルトはUTF-8> --compilation_level ADVANCED_OPTIMIZATIONS --jscomp_error=checkTypes --js <コンパイルするjaファイル> --js_output_file <コンパイル結果を保存するファイル>
- デフォルトでは型チェック機能はoffになっているので、「--jscomp_error=checkTypes」を指定してチェックを有効化する必要があります。
- 「--compilation_level」は型チェック目的では何でもよいですが、せっかくなので最強の「ADVANCED_OPTIMIZATIONS」を使います。
- 「--charset」は、指定しなければファイルをUTF-8として扱うので、指定しなくてもいいっちゃいい。
具体例。以下のようなjsファイル(sample.js)を用意し、
/** * 数値を受け付ける関数。 * @function * @param {number} number */ function acceptNumber( number ){} acceptNumber( "a" ); // コンパイルエラー!
コンパイラを実行すると、
$ java -jar compiler.jar --compilation_level ADVANCED_OPTIMIZATIONS --jscomp_error=checkTypes --js sample.js --js_output_file compiled.js sample.js:9: ERROR - actual parameter 1 of acceptNumber does not match formal parameter found : string required: number acceptNumber( "a" ); // コンパイルエラー! ^ 1 error(s), 0 warning(s), 100.0% typed
という感じで、エラーが出力されます。
クラス/インターフェイス
基本となる、クラス/インターフェイスの定義の方法と使い方について。コンパイラは、オブジェクトに関連付けられたクラス/インターフェイスに基づいて、プロパティのチェックや互換性のチェックを行います。
クラスの定義
「@constructor」を付与したメソッドは、クラスを生成するためのコンストラクタメソッドとなり、メソッド名に対応するクラス(以下の例であれば"sample.ClassA")が定義されます。
var sample = {}; /** * クラスA * @constructor */ sample.ClassA = function () {};
- 「new」付きで呼び出す
- クラスのインスタンスを「this」として呼び出す
ことのみ可能です。2は派生クラスから親クラスのコンストラクタを呼び出すときに使います。
new sample.ClassA(); // new 付きで呼び出すことは可能。 sample.ClassA.call( new sample.ClassA() ); // sample.ClassAのインスタンスをthisとして実行するのもOK sample.ClassA(); // コンパイルエラー! sample.ClassA.call( "a" ); // コンパイルエラー!
フィールド/メソッドの定義
フィールドやメソッドの定義は以下のとおり。
- フィールドの型は「@type {<型>}」で指定します。
- メソッドの引数は「@param {<型>} <引数名>」、戻り値は「@return {<型>}」で指定します。
/** * クラスA * @constructor */ sample.ClassA = function () { /** * クラスAのフィールド * @type {number} */ this.propertyA = 5; }; /** * クラスAのメソッド * @function * @param {number} arg1 引数1 * @return {string} 戻り値 */ sample.ClassA.prototype.methodA = function(arg1){ return "methodA"; }; /** * クラスAのフィールド2 * @type {number} */ sample.ClassA.prototype.propertyB;
クラスを使ってみます。
/** @type {sample.ClassA} */ // ローカル変数の型も@typeで明示します。 var a = new sample.ClassA(); a.propertyA = 10; a.propertyB = "a"; // コンパイルエラー! : 型が不一致 var n = a.propertyUndefined; // これはOK a.propertyUndefined = "a"; // これはOK a.methodA(10); a.methodA("a"); // コンパイルエラー! : 型が不一致 a.methodUndefined(10); // コンパイルエラー! : 未定義のメソッド
- クラスで定義されていないメソッドを呼び出したり、引数の型があわない場合コンパイルエラーとなります。
- フィールドの型があわない場合もコンパイルエラーとなります。
- 未定義フィールドへの代入や参照は可能とされているみたい。
delete a.propertyA; delete a.propertyB; delete a.methodA;
継承
「@extends {<親クラス>}」を付与すると、派生クラスとなります。
注意:「型システム上で派生クラスとみなされる」だけで、JavaScriptでの継承操作(「sample.ClassA2.ptototype = new sample.ClassA();」のような操作)は別途必要です。
/** * クラスAの派生クラス * @constructor * @extends {sample.ClassA} */ sample.ClassA2 = function () { sample.ClassA.call(this); // 親クラスのコンストラクタを呼び出す。 }; // JavaScriptでの継承操作は別途必要。 // ↓はClosure Libraryの機能を使って継承を実現する例。 goog.inherits(sample.ClassA2, sample.ClassA);
派生クラスなので、親クラスのメソッドやフィールドが使えるとみなされます。また、親クラス型の変数に代入できます。
/** @type {sample.ClassA2} */ var a2 = new sample.ClassA2(); // 親クラスのメソッド、フィールドにアクセスできる。 a2.propertyA = 10; a2.methodA(10); /** * クラスAと継承関係のないクラスB * @constructor */ sample.ClassB = function () {}; // 変数への代入 // ClassA型の変数には、ClassAおよびその派生クラスを代入できる。 /** @type {sample.ClassA} */ var classA = new sample.ClassA(); /** @type {sample.ClassA} */ var classA2 = new sample.ClassA2(); /** @type {sample.ClassA} */ var classB = new sample.ClassB(); // コンパイルエラー!
多重継承は不可です。
/** * クラスA,クラスBを多重継承 * @constructor * @extends {sample.ClassA} * @extends {sample.ClassB} // これはコンパイルエラー。 */ sample.ClassX = function () {};
インターフェイス
インターフェイスも作れます。「@interface」を付与すればOK。
var sample = {}; /** * インターフェイスA * @interface */ sample.InterfaceA; /** * インターフェイスのメソッド。 * 実装クラスが実装する必要がある。 * @function * @param {number} arg1 */ sample.InterfaceA.prototype.methodA; /** * インターフェイスの属性。 * これも実装クラスで実装する必要がある。 * @type {number} */ sample.InterfaceA.prototype.propertyA; /** * インターフェイスB * @interface */ sample.InterfaceB = function(){}; /** * @function * @param {number} arg1 */ sample.InterfaceB.prototype.methodB;
インターフェイスを実装するには、実装先のクラスに「@implements {<インターフェイス名>}」を付与します。
/** * インターフェイスA,Bを実装したクラス。 * @constructor * @implements {sample.InterfaceA} * @implements {sample.InterfaceB} // 複数のインターフェイスを実装できる。 */ sample.InterfaceAAndBImplemented = function(){ /** @override */ this.propertyA = "a"; }; /** @override */ sample.InterfaceAAndBImplemented.prototype.methodA = function(arg1){alert(arg1+"a");}; /** @override */ sample.InterfaceAAndBImplemented.prototype.methodB = function(arg1){alert(arg1+"b");};
使ってみます。
/** @type {sample.InterfaceA} */ var interfaceA = new sample.InterfaceAAndBImplemented(); // 実装クラスのインスタンスはインターフェイス型の変数に代入可能。 /** @type {sample.InterfaceB} */ var interfaceB = new sample.InterfaceAAndBImplemented(); interfaceA.propertyA = 10; interfaceA.propertyA = "x"; // コンパイルエラー! interfaceA.methodA(1); interfaceB.methodB(1); interfaceA.methodC("x"); // コンパイルエラー!
実装クラスで必要なメソッドや属性が実装されていない場合、コンパイルエラーとなります。
/** * sample.InterfaceA の実装クラス2。 * インターフェイスのメソッドが実装されていないため、コンパイルエラーになる。 * @constructor * @implements {sample.InterfaceA} */ sample.InterfaceAImplemented2 = function(){};
型の種類
型として指定できる値には、↑のようなユーザー定義クラス/インターフェイスのほか、number,stringといった「Value Type」や、組み込みのArray、Objectなどいくつか種類があります。
型の種類一覧
- * (All Type)
- number
- string
- boolean
- null
- undefined
- Object
- Array
- 関数型 (Function Type)
- レコード型 (Record Type)
- String,Number,Dateなどのビルトインクラス
- ユーザー定義クラス
Value Type
ECMAScriptのプリミティブ値 (Primitive Value)に対応する型で、
- number
- string
- boolean
- null
- undefined
があります。
- Value Type は Objectのサブタイプではなく、null以外のオブジェクトはObject型変数と代入互換性がありません。
- 別の型として「Number」「String」「Boolean」があり、こちらはECMAScriptにおける「Stringオブジェクト」や「Numberオブジェクト」に対応します。
- Value Typeと違ってObjectのサブタイプなので、Object型変数に代入できます。
- Value Typeとの互換性はありません。
/** * プリミティブの数値型変数 * @type {number} */ var primitiveNumberType; /** @type {number} */ var primitiveNumberVar = primitiveNumberType; /** @type {Number} */ var numberVar = primitiveNumberType; // コンパイルエラー! /** @type {Object} */ var objectVar = primitiveNumberType; // コンパイルエラー! primitiveNumberType = 1; primitiveNumberType = new Number(1); // コンパイルエラー! primitiveNumberType = null; // コンパイルエラー! /** * 数値オブジェクト型変数 * @type {Number} */ var numberType; /** @type {number} */ var primitiveNumberVar2 = numberType; // コンパイルエラー! /** @type {Number} */ var numberVar2 = numberType; /** @type {Object} */ var objectVar2 = numberType; numberType = 1; // コンパイルエラー! numberType = new Number(1); numberType = null;
Object Type
「Object」とその派生型。
- サブタイプとしてArray、Dateといった組み込みの型があります。
- ユーザー定義クラスもObject派生となります。
/**@type {Object}*/ var objectType; /**@type {Array}*/ var arrayType; /**@type {Date}*/ var dateType; objectType = {}; objectType = []; objectType = new Date(); arrayType = {}; // コンパイルエラー! arrayType = []; arrayType = new Date(); // コンパイルエラー! dateType = {}; // コンパイルエラー! dateType = []; // コンパイルエラー! dateType = new Date();
Function Type
関数を示す型です。
- 「{function(<パラメータの型...>):<戻り値の型>}」の形式で記述します。
- 結果を返さない関数の場合、戻り値型はvoidとします。
- Object派生です。
/** * 関数型の変数。 * {function(<パラメータの型...>):<戻り値の型>}の形式で指定する。 * * @type {function(string,number):string} */ var function1 = function( arg1, arg2 ) { return "aaa"; }; function1("a", 1); function1("a", "a"); // コンパイルエラー! : 第2引数の型が不一致 function1("a", 1, "a"); // コンパイルエラー! : 引数の数が不一致 /**@type {number}*/var var1 = function1("a", 1); // コンパイルエラー! : 戻り値の型が不一致 /** * 結果を返さない関数の場合、戻り値型はvoidとする。 * * @type {function(string,number):void} */ var function2 = function( arg1, arg2 ) { }; /**@type {number}*/var var2 = function2("a", 1); // コンパイルエラー!
Object派生なので、Object型変数に代入できます。また、関数型同士も以下の条件を満たしている場合サブタイプとみなされ、代入互換性を持ちます。
- 関数型AとBがあり、以下の条件を満たす場合、関数型Aは関数型Bのサブタイプとなる。
- Aの引数の型が、Bと同じかスーパータイプである。
- Aの戻り値型が、Bと同じかサブタイプである。
// 関数型の互換性確認 /** @type {function(Object):Object} */ var acceptObjectRetunObject; /** @type {function(Date):Object} */ var acceptDateRetunObject; /** @type {function(Object):Date} */ var acceptObjectRetunDate; // 以下の条件を満たす場合、サブタイプとみなされ代入互換性を持つ。 // - 引数の型が同じかスーパータイプである。 // - 戻り値が同じかサブタイプである。 /** @type {function(Object):Object} */ var acceptObjectRetunObject2 = acceptDateRetunObject; // コンパイルエラー! : 引数の互換性がない /** @type {function(Object):Object} */ var acceptObjectRetunObject3 = acceptObjectRetunDate; /** @type {function(Date):Object} */ var acceptDateRetunObject2 = acceptObjectRetunObject; /** @type {function(Date):Object} */ var acceptDateRetunObject3 = acceptObjectRetunDate; /** @type {function(Object):Date} */ var acceptObjectRetunDate2 = acceptObjectRetunObject; // コンパイルエラー! : 戻り値の互換性がない /** @type {function(Object):Date} */ var acceptObjectRetunDate3 = acceptDateRetunObject; // コンパイルエラー! : 戻り値,引数共に互換性がない
ECMAScript4のオーバービューでは、引数の数も同じでないとダメとなっていますが、Closure Compilerでは互換性があれば違っていてもOKのようです。
// 引数の数は違っていてもOK /** @type {function(string):string} */ var acceptString; /** @type {function(string,number):string} */ var acceptStringAndNumber; /** @type {function(string):string} */ var acceptString2 = acceptStringAndNumber; /** @type {function(string,number):string} */ var acceptStringAndNumber2 = acceptString;
All Type
All Typeはすべての型のスーパータイプです。
- 型として、「{*}」を指定すると、All Typeになります。
- すべての型のスーパータイプなので、All Type型の変数にはすべての値を代入できます。
- 定義されているかどうか不明なメソッドを呼び出してもコンパイルエラーにはなりません。
- この挙動はどうなんだろう・・・。すべての型のUnion(後述)的な扱いなのかな・・・。
/** * 任意の値を代入可能な変数。 * @type {*} */ var allType; // すべての型の親クラス扱いなので、stringやObject型の変数に代入できない。 /** @type {string} */ var stringVar = allType; // コンパイルエラー! /** @type {number} */ var numberVar = allType; // コンパイルエラー! /** @type {Object} */ var objectVar = allType; // コンパイルエラー! // 定義されているかどうか不明なメソッドを呼び出してもコンパイルエラーにはならない。 allType.foo(); allType.foo2(); // あらゆる値を代入可能。 allType = "a"; allType = 1; allType = []; allType = function(){}; allType = false; allType = undefined; allType = null;
ECMAScript4のオーバービューでは、"Any"になってますね。
Record Type
「規定されたプロパティ一式を持つオブジェクト」を示す型です。
- 「{<プロパティ名>:<型>,....}」の形式で記述します。
- オブジェクトが、指定された型と名前のプロパティをすべて持つ場合、この型にマッチします。
/** * 文字列型のプロパティaと関数型のプロパティbを持つオブジェクトを格納可能な変数。 * @type {{a:string,b:function():string}} */ var hasAAndB; hasAAndB = {a:"aaa", b:function(){return "b";}}; hasAAndB = {a:"aaa"}; // コンパイルエラー! : プロパティbがない hasAAndB = {a:"aaa", b:5}; // コンパイルエラー! : プロパティbの型が不一致 hasAAndB = {a:"aaa", b:function(){return "b";}, c:5}; // 属性が多いのはOK hasAAndB.a = "a"; hasAAndB.b(); hasAAndB.x(); // コンパイルエラー! hasAAndB.x = "x"; // これはOK delete hasAAndB.x; // これもOK
レコード型は、
- 「互換性があり、かつ、より制約条件の緩い」レコード型のサブタイプとなります。
- 上記条件を満たしていれば、クラスのインスタンスもサブタイプと判定されます。
- ECMAScript4にあるlike演算子とかはつけなくてもよいみたい。
// 代入互換の確認 // 制限が緩くなり、型安全でなくなるパターンはコンパイルエラーとなる。 /** @type {{x:string,y:number}} */ var hasXAndY; /** @type {{x:string}} */ var hasX; /** @type {{y:number}} */ var hasY; /** @type {{x:string,y:number}} */ var hasXAndY2 = hasX; // コンパイルエラー! /** @type {{x:string,y:number}} */ var hasXAndY3 = hasY; // コンパイルエラー! /** @type {{x:string}} */ var hasX2 = hasXAndY; /** @type {{x:string}} */ var hasX3 = hasY; // コンパイルエラー! /** @type {{y:number}} */ var hasY2 = hasXAndY; /** @type {{y:number}} */ var hasY3 = hasX; // コンパイルエラー! // クラスも互換性があれば型にマッチする。 var sample = {}; /** @constructor */ sample.ClassA = function(){ /** @type {string} */ this.x = ""; } /** @constructor */ sample.ClassB = function(){} /** @constructor */ sample.ClassC = function(){ /** @type {number} */ this.x = 1; } hasX = new sample.ClassA(); hasX = new sample.ClassB(); // コンパイルエラー! : プロパティがない hasX = new sample.ClassC(); // コンパイルエラー! : プロパティはあるが型が不一致
型の結合
Union Typeとして、「任意の型のいずれか」を示す型を記述できます。
- 「{(<型1>|<型2>)}」の形式で、結合する型を「|」区切りで並べればOK。
- ECMAScript4のものとは微妙に記法が違います。
/** * boolean または number を代入可能な変数 * @type {(boolean|number)} */ var booleanOrNumber; booleanOrNumber = false; booleanOrNumber = 1; booleanOrNumber = "a"; // コンパイルエラー!
Union Typeは、「結合した型すべてと同じかまたはスーパータイプ」となる型のサブタイプとなります。具体的には以下のサブタイプとなります。
- 結合した型またはそのスーパータイプをすべてを含む別のUnion Type
- 結合した型すべてに共通する親クラス
// 以下の派生関係にあるクラスを作る。 // ClassA // + ClassB // + ClassC // ClassX /** @constructor */ sample.ClassA = function(){} /** @function */ sample.ClassA.prototype.methodA = function(){alert("a");}; /** @type {string} */ sample.ClassA.prototype.propertyA = "aa"; /** * @constructor * @extends {sample.ClassA} */ sample.ClassB = function(){} /** @function */ sample.ClassB.prototype.methodB = function(){alert("b");}; /** @type {string} */ sample.ClassB.prototype.propertyB = "bb"; /** * @constructor * @extends {sample.ClassA} */ sample.ClassC = function(){} /** @function */ sample.ClassC.prototype.methodC = function(){alert("c");}; /** @type {string} */ sample.ClassC.prototype.propertyC = "bb"; /** @constructor */ sample.ClassX = function(){} /** @function */ sample.ClassX.prototype.methodX = function(){alert("x");}; /** * sample.ClassB または sample.ClassC を代入可能な変数 * @type {(sample.ClassB|sample.ClassC)} */ var BorC; //結合した要素またはスーパータイプをすべて持つ別のUnion Typeのサブタイプとなる。 /** @type {(sample.ClassB|sample.ClassC|sample.ClassX)}*/ var BorCorX = BorC; /** @type {(sample.ClassA|sample.ClassX)}*/ var AorX = BorC; // sample.ClassA はsample.ClassB,sample.ClassC両方のスーパータイプなのでOK /** @type {(sample.ClassC|sample.ClassX)}*/ var CorX = BorC; // コンパイルエラー! // 結合した要素の共通の親のサブタイプとなる。 /** @type {sample.ClassB}*/ var b = BorC; // コンパイルエラー! /** @type {sample.ClassC}*/ var c = BorC; // コンパイルエラー! /** @type {sample.ClassA}*/ var a = BorC; /** @type {Object}*/ var o = BorC; /** @type {sample.ClassX}*/ var x = BorC; // コンパイルエラー!
メソッドやフィールドの扱いはどうなるかというと、現状では特にチェックされていないようです・・。仕様なのか未実装なのか、それともバグなのかは不明。
/** * sample.ClassB または sample.ClassC を代入可能な変数 * @type {(sample.ClassB|sample.ClassC)} */ var BorC; // メソッド/フィールドのチェックは行われていない模様。 // 仕様なのか未実装なのか、それともバグなのかは不明・・・。 BorC.methodA(); BorC.methodB(); // コンパイルエラーになるかも、と思ったがエラーにはならず。 BorC.methodC(); // コンパイルエラーになるかも、と思ったがエラーにはならず。 BorC.methodX(); // ClassB,ClassCのどちらでも定義されておらず明らかに不正だが、コンパイルエラーになったりはしない・・・。 BorC.propertyA = 1; BorC.propertyB = 1; // コンパイルエラーになったりはしない・・・。 BorC.propertyX = 1; // コンパイルエラーになったりはしない・・・。
型パラメータ
型パラメータを使って、配列の要素やObjectのキーと値の型を制限できます。
- ECMAScript4では、型パラメータを受け付けるクラスやインターフェイスが作れるようだけど、
- Closure Compilerのドキュメントでは説明なし・・・。現状では、配列とObjectでのみ使えるようです。
var sample = {}; /** * @constructor */ sample.ClassA = function(){}; // 配列の要素の型を指定 /** @type {Array.<string>} */ var stringArray = []; /** @type {Array.<number>} */ var numberArray = []; /** @type {Array.<sample.ClassA>} */ var objectArray = []; stringArray[0] = ""; stringArray[1] = 1; // コンパイルエラー! stringArray[2] = new sample.ClassA(); // コンパイルエラー! numberArray[0] = ""; // コンパイルエラー! numberArray[1] = 1; numberArray[2] = new sample.ClassA(); // コンパイルエラー! objectArray[0] = ""; // コンパイルエラー! objectArray[1] = 1; // コンパイルエラー! objectArray[2] = new sample.ClassA(); /** @type {string} */ var string = stringArray[0]; /** @type {number} */ var number = stringArray[0]; // コンパイルエラー // Objectのキーと値の型を指定する。 /** @type {Object.<string, string>} */ var stringMap = {}; stringMap["a"] = "a"; stringMap[1] = "b"; // コンパイルエラー! stringMap["b"] = 1; // コンパイルエラー! /** @type {string} */ var stringVar = stringMap[""]; /** @type {number} */ var numberVar = stringMap[""]; // コンパイルエラー!
型パラメータ付きオブジェクトの代入互換性
試してみた限り、特にチェックはされていない感じですねー。いいのかな。
/** * ClassAの派生クラス * @constructor * @extends {sample.ClassA} */ sample.ClassB = function(){}; /** * ClassAと継承関係のないクラス * @constructor */ sample.ClassC = function(){}; //スーパータイプやサブタイプを要素に持つ配列に代入してもエラーにならず・・・。 /** @type {Array.<Object>} */ var parentArray = objectArray; /** @type {Array.<sample.ClassB>} */ var subtypeArray = objectArray; /** @type {Array.<sample.ClassC>} */ var othertypeArray = objectArray; /** * ClassAを要素に持つ配列を受け付けるメソッド * @param {Array.<sample.ClassA>} arg1 */ function acceptArrayOfClassA(arg1) {alert(arg1.length);}; // スーパータイプやサブタイプを要素に持つ配列を渡してもエラーにならず・・・。 acceptArrayOfClassA(parentArray); acceptArrayOfClassA(subtypeArray); acceptArrayOfClassA(othertypeArray);
型パラメータの制約やvariance annotationとかもなし。これらはECMAScript4でも用意されていませんね。
Nullable
型の前に「!」または「?」を付与することで、「値をnullにできるかどうか」を指定できます。
- 「!」を付与すると、null不可、
- 「?」を付与すると、null可( {(<型>|null)}と同じ扱い )になります。
- 明示しない場合のデフォルト動作は以下のとおりです。
- 関数型、string、number、booleanはnull不可
- その他はnull可
/** * プリミティブの数値型変数 * @type {number} */ var primitiveNumberType; /** * null可のプリミティブの数値型変数 * @type {?number} */ var nullablePrimitiveNumberType; /** * null不可のプリミティブの数値型変数 * @type {!number} */ var nonNullablePrimitiveNumberType; primitiveNumberType = 1; primitiveNumberType = null; // コンパイルエラー! nullablePrimitiveNumberType = 1; nullablePrimitiveNumberType = null; nonNullablePrimitiveNumberType = 1; nonNullablePrimitiveNumberType = null; // コンパイルエラー! // 指定しない場合の、デフォルト動作の確認。 // 関数型、string、number、booleanはnull不可となる。 /** * @type {string} */ var stringType; /** * @type {function():void} */ var functionType; /** * @type {Object} */ var objectType; functionType = null; // コンパイルエラー! stringType = null; // コンパイルエラー! objectType = null;
特殊な引数
引数の型として指定できる特殊な記法について。関数型の引数部分、またはクラスやインターフェイスメソッドの@paramの型部分で使えます。
可変長引数
「...[<型>]」で可変長の引数を指定できます。
/** * 可変長引数を受け付ける関数 * @type {function(...[string]):string} */ var acceptVarArgs = function() { return "aaa"; }; acceptVarArgs("a"); acceptVarArgs("a", "b", "c"); acceptVarArgs(); // 引数なしもOK acceptVarArgs("a", 1, "b"); // コンパイルエラー! // @paramタグで使用する場合、[]は不要。 var sample = {}; /** * @constructor */ sample.ClassA = function(){}; /** * @function * @param {...string} args 可変長引数 */ sample.ClassA.prototype.acceptVarArgs = function(args){}; /**@type {sample.ClassA}*/ var a = new sample.ClassA(); a.acceptVarArgs("a"); a.acceptVarArgs("a","b","c"); a.acceptVarArgs(); a.acceptVarArgs("a", 1, "b"); // コンパイルエラー!
指定してもしなくてもよい引数
型の後に「=」を付与すると、指定してもしなくてもよい引数となります。
/** * 指定してもしなくてもよい引数を受け付ける関数 * @type {function(string,number=):string} */ var acceptOptionalArg = function() { return "aaa"; }; acceptOptionalArg("a", 1); acceptOptionalArg("a"); acceptOptionalArg("a", "b");// コンパイルエラー! acceptOptionalArg("a", null);// コンパイルエラー! var sample = {}; /** * @constructor */ sample.ClassA = function(){}; /** * @function * @param {string} arg1 * @param {number=} arg2 指定してもしなくてもよい引数。 */ sample.ClassA.prototype.acceptOptionalArg = function(arg1,arg2){}; /**@type {sample.ClassA}*/ var a = new sample.ClassA(); a.acceptOptionalArg("a", 1); a.acceptOptionalArg("a"); a.acceptOptionalArg("a", "b");// コンパイルエラー! a.acceptOptionalArg("a", null);// コンパイルエラー!
型定義
「@typedef」を使って、型を定義できます。
var sample = {}; // 型を定義 /** * @typedef {{each:function(*):void}} */ sample.Enumerable; // 定義した型を使うメソッド /** * map関数 * @param {sample.Enumerable} enumerable * @return {Array.<*>} */ function map( enumerable ) { var result = []; enumerable.each( function(item){ result.push(item); }); return item; } var array = map({ each : function( f ) { for ( var i=0;i<10;i++) f(i); } }); // 以下はコンパイルエラー! : sample.Enumerable型にマッチするeachがない map({ each : "" });
Record TypeやUnion Typeを再利用する場合に使う機能みたいですね。
インターフェイスやクラスを使う手もありますが、必要なメソッド数が少ない場合はこちらのほうがシンプルに書けそうかな。
型推論
ちゃんとしたキュメントが見つけられなかったのであれですが、サンプルコードを書いて試していると「これ型推論してるんじゃね?」と思わせる挙動がいくつかあったので紹介。
// 変数の型が明示されていなくても、代入された値から型を推論してチェックする。 var array = ["a","b"]; array.join("x"); var n = array.length; array.undefinedMethod(); // コンパイルエラー。 // 変数の型が明示されていても、代入された値があればその型に応じてチェックが行われる。 /** @type {Object} */ var object = new Date(); object.getTime(); object.undefinedMethod(); // コンパイルエラー。 object = ["a","b"]; object.join("x"); n = object.length; object.undefinedMethod(); // コンパイルエラー。 // 関数の戻り値から推論 /** * @function * @return {Date} */ function now() { return new Date(); }; var d = now(); d.getTime(); d.undefinedMethod(); // コンパイルエラー。
感想など
JavaScriptコードも規模がそこそこ大きくなってくると静的な型チェック機能が欲しくなってきますよね?
- numberのはずのIDがいつの間にかstringになっていてエラー
- ライブラリAPIに渡す引数の順番を間違えていてエラー
といった不毛なバグを回避したいなら、利用を検討してみてはどうでしょうか?特に中規模以上プロジェクトで複数人で開発するような場合、効果はあるかと思います。タイプセーフコーディング以外にも「型情報を使用したJavaScriptコードの最適化」とかも期待できるかもです。(このへんはぜんぜん見れていませんが・・・)
利用にあたって問題になりそうなことは、使用するライブラリの対応ですかね。ライブラリに型情報が記載されていないと機能しないので、JsDocがちゃんと書かれていないライブラリだとまったく使えない・・・。実質的にClosure Libraryを使うことになるのですが、絶賛開発中な感じなのがちょっと・・・。Collectionクラスは妙に充実していますが。あとは、prototype.jsやdojoにあるようなクラス定義ユーティリティとの相性も気になるところです。