TypeScript で関数にプロパティをはやしたい

この内容は TypeScript 0.9.1 をもとに記述しています。

TypeScript の 0.9.1.1 のブランチが切られていて、もうそろそろ登場しそうな今日この頃です。

表題の件、 生 JavaScript なら結構やることなのですが、 TypeScript でやると怒られます。例えば以下のようなコードを書いたとして、

function greeting(): string{
    return "hello";
}

greeting.name = function(): string{
    return "k_maru";
}

コンパイルすると、以下のように怒られます。

error TS2094: The property 'name' does not exist on value of type '() => string'.

さて、以下のようなコードの書き方をさせたいとします。

//キャッシュを設定する
Lib.cache("user", {
   name: "hoge"
});

//キャッシュから取得
var user = Lib.cache("user");

//キャッシュの設定
Lib.cache.defaultDefinition({
    maxSize: 10,
    expire: 10000
});

Lib.cache 関数でキャッシュの値の設定と取得をさせてます。で、Lib.cache 関数からはえた defaultDefinition 関数でキャッシュの設定を行ってます。まぁ、こまかい突っ込みは置いといて、よくやるコードですよね。多分。
でもこれ、 TypeScript で普通に書くと以下のような感じになると思うのですが、最初の例の通りに怒られてしまいます。

module Lib{

  var items: {[index: string]: any;} = {};

  export function cache(name: string, value?: any): any{
    if(!value){
      if(name in items){
        return items[name];
      }
      return;
    }
      items[name] = value;
  }

  cache.defaultDefinition = function(def: any): void{
    //set definition
  }
}
The property 'defaultDefinition' does not exist on value of type '(name: string, value?: any) => any'.

まぁ、はやせないものは仕方がないのですが、 TypeScript 0.9 から Declaration Merging って機能で module を関数にすることができるようになってます。ってことで、早速つかってみます。

module Lib{

  var items:{[index: string]: any;} = {};

  export function cache(name:string, value?:any):any {
    if (!value) {
      if (name in items) {
        return items[name];
      }
      return;
    }
    items[name] = value;
  }

  export module cache{
    export function defaultDefinition(def:any):void {
      //set definition
    }
  }
}

これで無事にコンパイルが通って、思ったようなコードの書かせ方ができるようになりました。すべての場合にこれで対応できるかどうかはなぞっていうか難しい気がしますが、まぁ OK でしょう。以下のような JavaScript がはかれてます。

var Lib;
(function (Lib) {
  var items = {};

  function cache(name, value) {
    if (!value) {
      if (name in items) {
        return items[name];
      }
      return;
    }
    items[name] = value;
  }
  Lib.cache = cache;

  (function (cache) {
    function defaultDefinition(def) {
      //set definition
    }
    cache.defaultDefinition = defaultDefinition;
  })(Lib.cache || (Lib.cache = {}));
  var cache = Lib.cache;
})(Lib || (Lib = {}));

って、ここで終わったら万々歳なんですが、もうちょっと話を進めてみて、いまは cache 関数で設定されている生の値を返してますが、 生の値を保持した CacheItem クラスのインスタンスを返したいとします。これも結構よくやりますよね。多分。

以下のようなコードを書かせたいとします。

//キャッシュを設定すると、 CacheItem が返ってくる
var cacheItem = Lib.cache("user", {
  name: "hoge"
});
//キャッシュを取得すると、 CacheItem が返ってくる
cacheItem = Lib.cache("user");
//CacheItem には色々プロパティがあって、状態とか条件を確認したり変更したりできる
if(!cacheItem.expired){
  cacheItem.expirationDateTime = new Date(now + (1000 * 60 * 60));
}
//もちろん値も取れる
var user = cacheItem.value();

Lib.cache.defaultDefinition({
  maxSize: 10,
  expire: 10000
});

実装としては cache 関数は CacheItem クラスのインスタンスを返すんやけど、シグネチャとしてはインターフェイスである ICacheItem としたい。 CacheItem クラスは外から見えないようにして、 new させたくないってところです。 なぜって、 JavaScript で new するのってなんか気持ち悪いってだけですが。

さらに、 ICacheItem インターフェイスは Lib モジュールの下にいるんじゃなくって、 Lib.cache モジュールの下にいるようにします。 Lib.cache は関数であり同時に名前空間的なモジュールなので、そっちのほうがきれいです。

module Lib{

  var items:{[index: string]: cache.ICacheItem;} = {};

  // ICacheItem を返す
  export function cache(name:string, value?:any): cache.ICacheItem {
    if (!value) {
      if (name in items) {
        return items[name];
      }
      return;
    }
    // CacheItem を作る
    var item = new CacheItem(value);
    items[name] = item;
    return item;
  }

  export module cache{
    export function defaultDefinition(def:any):void {
      //set definition
    }
    export interface ICacheItem{
      //properties
    }
  }
  // cache モジュールの下に作ると、 cache 関数から見えるようにするためには
  // export しないとだめやけど、 export すると全体に見えてしまうから
  // Lib モジュールの下に作る
  class CacheItem implements cache.ICacheItem {
    constructor(public value: any){}
    //properties
  }
}

CacheItem クラスが Lib モジュールの下で、実装しているインターフェイスの ICacheItem が Lib モジュールの下の cache モジュールってので、上の階層が下の階層を見てるってので気持ち悪いですが、まぁ、仕方がないです。

そんなこんなで、TypeScript の時はインターフェイスの考え方をちょっと JavaScript とは変える必要があるかなぁと思ったり思わなかったり。TypeScript だけですべて書ききる、かつ他の JavaScript ライブラリも使わないのであれば、 new が気持ち悪いとかないと思うのですがどうしてもやっぱり混ざっちゃうのですよね。そうした場合にできる限り書かせ方は近いほうがよいと考えてて、合わせようとすると、 TypeScript 側に無理が出てしまうので、どっちに重きを置くかの違いなのですがなかなか悩ましい。


最後はだいぶタイトルから話がそれたような気はしますが、以上。