読者です 読者をやめる 読者になる 読者になる

Closure Compilerの型テンプレートが進化してる件

altjsガチ勢のみなさんこんにちは。

TypeScriptの次期バージョンでジェネリクスが入るぜ!なんて話題になってたりしますが、Closure Compilerのテンプレートまわりも人知れず進化しているので紹介。

なぜかドキュメントにはさっぱり載ってないので、Compilerのソースとテストコードが命です。

メソッドテンプレート

去年サポートされたのがメソッドテンプレート。メソッドや関数のアノテーション@templateが使えます。

次のサンプルコードはArrayから特定条件を満たす要素を探して返す関数。Arrayの要素の型と条件関数fthisにバインドされるopt_thisの2つがテンプレートになってます。

/**
 * @param {Array.<T>} arr
 * @param {function(this: S, T, number, Array.<T>): boolean} f
 * @param {S=} opt_this
 * @return {?T}
 * @template T,S
 */
function find(arr, f, opt_this) {
  for (var i = 0; i < arr.length; i++) {
    if (f.call(opt_this, arr[i], i, arr)) {
      return arr[i];
    }
  }
  return null;
}
 
/** @type {Array.<number>} */
var a = [10, 20, 30];
var b = find(a, function(item, index, arr) {
  return item > 20; 
});
 
/** @type {string} */
var c = b; // Type Error! Required {string} but found {number}

テンプレートのおかげでbnumberで型付けされるので、string型のcに代入しようとすると型エラーになるわけです。

このへんの使い方は、Closure Libraryのgoog.arrayとかgoog.objectのコードが参考になります。

クラステンプレート

続いて今月のリリースでサポートされたクラステンプレート。 オンラインの Closure Compiler Service はまだ最新じゃないので、クラステンプレートを試したい人はローカルで動かしてください。

あんまり意味無いコードですけどサンプル。

/**
 * @param {T} prop
 * @template T
 * @constructor
 */
var Foo = function(prop) {
  /**
   * @type {T}
   * @private
   */
  this.prop_ = prop;
};
 
/**
 * @return {T}
 */
Foo.prototype.getProp = function() {
    return this.prop_;
};
 
/** @type {Foo.<number>} */
var foo = new Foo(100);
 
/** @type {string} */
var bar = foo.getProp(); // Type Error! Required {string} but found {number}

アノテーションFoo.<number>型を宣言してるので、foo.getProp()の戻り値の型はnumberになるわけです。よってstring型のbarに代入するところで正しく型エラー。

newのところの型宣言アノテーションは必須。型推論がもうちょい効いてくれるとうれしいですね。

インターフェイスも同じような感じ。

/**
 * @template T
 * @interface
 */
var A = function() {}; 

/**
 * @implements {A.<number>}
 * @constructor
 */
var B = function() {}; 

/** @type {A.<number>} */
var b1 = new B(); // OK

/** @type {A.<?>} */
var b2 = new B(); // OK

/** @type {B} */
var b3 = new B(); // OK

/** @type {A.<string>} */
var b4 = new B(); // Type Error! Required {A.<string>} but found {A.<number>}

A<?>みたいなワイルドカードでも受けられるけど、型変数の中身が違ってたらちゃんとエラーになります。


という感じで、1年ちょっと前はテンプレートはなぜかthisにしかバインドできないという謎仕様だったのがだいぶ進化してきました。 @extends {A.<string>}とか@implements {A.<string>}みたいにクラスの型変数をバインドしても、親クラスのメソッドの型変数に反映されないのが微妙なので、このあたりがうまくいけば満足できそうです。あと型推論か。