レガシーなJavaScriptコードをTypeScriptを使って整備したメモ

TypeScriptはレガシーなJavaScriptコードを安全に書き換えるのにも使えるよというメモ。

整備した目的

いちから書き直す時間をもらえない状況だったので短時間で安全に書き換える必要があった。そこでTypeScriptに書き換えてみたところ、書き換えたい処理がコンパイルエラーとして検出されたので簡単かつ安全に整備することができた。

TypeScriptを選んだ理由

  • 型宣言を明示的に追加していくことでレガシーコードを安全に把握したいため
  • 多くのエディタでサポートが充実している
  • そこそこ読めるJavaScriptを吐くのでTypeScript自体を捨てやすい

書き換えた手順と今後やりたいこと

  1. 拡張子を.js -> .tsに書き換えてコンパイルが失敗することを確認する
  2. ソースコードが依存しているグローバル変数に対してアンビエント宣言を追加していく
  3. prototype.js由来のメソッドをprototypeやグローバル変数を汚染しない関数の呼び出しに置き換える
  4. グローバル変数を外から注入するようにする
  5. 型宣言を追加し --noImplicitAnyが通るようにする
  6. テストを追加してリファクタリング
  7. TypeScriptを捨てる?

1.拡張子を.js -> .tsに書き換えてコンパイルエラーを確認した

mv legacy.js legacy.ts
tsc legacy.ts

エラーが検出されるのを確認する

legacy.ts(392,22): error TS2304: Cannot find name 'SomeGlobalSettingVar'.
legacy.ts(127,65): error TS2304: Cannot find name 'Enumerable'.
legacy.ts(337,13): error TS2304: Cannot find name 'Ajax'.
legacy.ts(392,22): error TS2304: Cannot find name 'Class'.
#...300行ぐらいエラーが出る

これらのエラーを修正していくことで安全に整備していった。

2. ソースコードが依存しているグローバル変数に対してアンビエント宣言を追加していく

TypeScriptコンパイラは宣言されていない変数にアクセスする処理をコンパイルエラーにしてくれる。 このエラーによってグローバル変数に依存する処理を検出することができた。

var id = SomeGlobalSettingVar.id; //legacy.ts(392,22): error TS2304: Cannot find name 'SomeGlobalSettingVar'.

この処理をコンパイルに通すためにはソースコードアンビエント宣言を追加する必要がある。アンビエント宣言というのはソースコード外で初期化される変数を明示的に宣言するための仕組みのことだ。 今回の整備ではグローバル変数コンパイルエラーとして検出するたびに、このアンビエント宣言をレガシーコードの先頭に追加していった。

interface SomeGlobalSettingVar {
    id: number;
}

declare var SomeGlobalSettingVar: SomeGlobalSettingVar;

こうすることで依存しているグローバル変数名とそのシグネチャを明確にすることができた。

3.prototype.js依存の処理をprototypeやグローバル変数を汚染しない形に書き換えて、呼び出し元を修正する

TypeScriptコンパイラはブラウザに組み込まれている関数の情報を持っているので、prototype.jsが組み込みオブジェクトを勝手に拡張した箇所を検出してくれる。下の例ではString.prototype.sliceは元からStringに生えているメソッドなのでコンパイルが通るが、String.prototype.succはprototype.js由来のメソッドなのでエラーになっている。

var someString = "some string";
var foo = someString.slice(0, 2); //ok
var bar = someString".succ(); //legacy.ts(9,16): error TS2339: Property 'succ' does not exist on type 'string'.

prototype.js依存の処理をグローバル変数やprototypeを汚染しない形に書き換えて、呼び出し元を修正する。

function succ(str: string): string {
    return str.slice(0, str.length - 1) +
      String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
}

var bar = succ("someString");

これを繰り返すことでprototype.jsを使わないように書き換えることができた。 さらに、prototype.jsの機能のうち使っているものだけを切りだしたので全体のファイルサイズを大きく削減することができた。

4.グローバル変数を外から注入するようにする

2と3を繰り返すことでTypeScriptのコンパイルが通るようになった。 依存しているグローバル変数を全てファイルの先頭にアンビエント宣言として一箇所に列挙することができたので、それらの変数を一つにまとめて関数の引数として注入するように書き換えた。 ひとまずグローバル変数prototype.jsの依存を断つという目標を達成できたので実際の業務では一旦終わりとした。 ここから下は実際にはやっていないが今後やりたいことを列挙してみる。

5.型宣言を追加し --noImplicitAnyが通るようにする

今後レガシーコードに対してこれからも機能追加があるようなら、より整備しやすいように修正するのがいいだろう。例えば変数に型注釈を追加していって型カバレッジ?を上げていくとよりリファクタリングが安心してできるようになるはずだ。 型宣言が足りなくて暗黙的にany型になった変数があればコンパイルエラーになる--noImplicitAny オプションをつけてコンパイルが通るようにすることを目標にするのが良いと思う。

6.テストを追加してリファクタリング

ひと通りコードを把握することができたらレガシーコードを片手にひたすらテストを書いてリファクタリングもしたい。

7.TypeScriptを捨てる?

もしTypeScriptが廃れた場合は出力されたJavaScriptコードだけを保守するようにすればよい。TypeScriptが出力するJavaScriptコードはそこそこ読める。ES6コードとして出力したコードをbabelを使って開発するという手も考えられる。

リファクタリング・ウェットウェアを読んだメモ

リファクタリング・ウェットウェアを読んだのでメモ。 この本は上達するとはどういうことかについてや、効率的な学習方法について紹介する本。

自分が特に気になったのは学習の部分について。 人間の学習にもTDD的な方法論は使えるというのが発見だった。 TDDのテストに相当するのが目標で以下のように目標を設定すると良いのだとか。

  • 適切な粒度に分割する
  • 数値でassert可能な目標を設定する
  • timeoutを設定する

TDDでいうリファクタリングは自分の知っている情報から「全体像」をつかむということらしい。知識を整理するにはマインドマップを書くといいよという話だった。

f:id:ganr:20150105205356p:plain

メモ代わりにとった写真を雑に貼っておく。スキャナが欲しい。 単語を並べたマインドマップは他人から見たらカッコ良い単語を並べただけのように見えて悪い意味で意識高い感じになるね。

f:id:ganr:20150105210529j:plain

f:id:ganr:20150105210534j:plain

リファクタリング・ウェットウェア ―達人プログラマーの思考法と学習法

リファクタリング・ウェットウェア ―達人プログラマーの思考法と学習法

TypeScriptの型定義ファイルを作るときに行儀の悪いコードもコンパイルできるようにするべきか?

追記あり。(ついでにタイトルを「TypeScriptの型定義で悩んでいること」から改題) TypeScriptの型定義で悩んでいること。

前提

  • 型定義を作ろうとしているライブラリは自分が作ったものではない
  • せっかくなのでDefinitelyTypedにプルリクエストを送りたい

悩んでいること

emitter.addListeners({
  "click": function(){/* ... */},
  "keyup": function(){/* ... */}
})

上のようなキーバリューのペアで設定する引数のためにインターフェイスを定義したくなった。

interface EventsListeners {
  [event: string]: Function
}

こうすることでリスナーに関数以外のものを指定して実行時例外になってしまうコードをコンパイル時に検出できる。 しかし、これだとコンパイルが通らないケースがあることに気づいた。

class MyEventListeners {
  constructor(public click: Function);
}

var onClick = function(){ console.log("The event was raised!"); };
var listeners = new MyEventListeners(onClick);
emitter.addListeners(listeners);

行儀の悪い使い方だと思うけど、内部でfor(i in listeners) + hasOwnPropertyで回しているので動いてしまう。 両方の例をコンパイルに通すためにはObject型を指定しないといけないはず。 型をより厳しく書くために下のケースのようなコードはエラーにしてしまいたいけど、DefinitelyTypedに置くのならJavaScriptでできることは全てコンパイルを通るようにしないといけないのだろうか?

追記

この記事の内容を@vvakameさん(DefinitelyTypedのコラボレータ)に質問したところ、いくつかアドバイスをいただきました。(今回型定義を作ろうとしたファイルはeventemitter

これらはあくまでvvkameさんの場合ということで、明確な基準はないそうです。とはいえひとつの方針としてこれから型定義を作るときの参考になりますね。

あるファイルに依存するファイルを返すgulpプラグインgulp-resolve-dependentsを作った

与えられたファイルを参照しているファイル群をパイプラインに流してくれるプラグインgulp-resolve-dependentsを作った。 サンプルはこんな感じ。

var gulp = require('gulp'),
    sass = require('gulp-sass'),
    path = require('path'),
    resolveDependents = require('gulp-resolve-dependents');

 gulp.src('./src/lib/example.scss')
.pipe(resolveDependents({
      files: './src/**/*.scss',
      resolver: sassImportResolver,
      basePath: './src'
}))
.pipe(sass())
.pipe(gulp.dest('./dest/css'));

function sassImportResolver(filePath, fileContents){
  var match, result = [],
      pattern = /import "(.+?)";/mg;
  while((match = pattern.exec(fileContents)) !== null){
    result.push(path.resolve(path.dirname(filePath), match[1]));
  }
  return result;
}

gulp-resolve-dependeciesというプラグインを参考にしたのだが、これはあるファイルが依存しているファイルをパイプラインに流してくれる。

一方gulp-resolve-dependentsは逆方向に依存性を解決する。

例えば以下のような依存関係になっているとする。 依存解決前

sourceがパイプラインから流れてくると、対象となるファイル(dependent)は下の図のようになる。

依存解決後

依存の解決方法自体はこのプラグインを使う人が外から渡すようになっているので、TypeScriptやsassなど任意のプロジェクトに対して使うことができる。

自分はこのプラグインをTypeScriptのコンパイル用に使っていて、gulp-filterと組み合わせて実際にhtmlから読み込まれるTypeScriptファイルのうち、今編集したファイルが影響するものだけをコンパイルするようにしている。

TypeScript+ブラウザ向けプロジェクトのためのテンプレートエンジン Chonmage を作った

TypeScriptの勉強用に小さなテンプレートエンジンを作った。 型チェックができるmustacheを目指して作り始めたんだけど、そもそもmustacheが型にゆるふわ過ぎる仕様だったので途中で後悔した。結局mustacheの仕様からいろいろ削って何とか形にはなったので公開してみる。

こんな感じ

テンプレートエンジン Chonmage (https://github.com/ryiwamoto/chonmage) Chonmageを使ったプロジェクトのサンプル(https://github.com/ryiwamoto/chonmage-sample)

テンプレートに渡すデータのinterface

テンプレートファイル

生成されるTypeScriptコード

テンプレートの描画

Chonmageの特徴

テンプレートを取得するためのキーやテンプレートに渡した変数がインターフェースを満たしているかをコンパイル時にチェックできる

これは「特殊化されたオーバーロードと文脈依存型推論の合わせ技(http://b.hatena.ne.jp/entry/qiita.com/progre/items/6b85993af5b4a7f1e792)」をつかっている。

Chonmageではライブラリが

interface ITemplateStore{
    get(key: string): void;
}

というテンプレートをどんなキーで取得しても常にvoidを返すインターフェースを提供していて、

各テンプレートをコンパイルするごとにキー名とコンパイルされたインスタンスを紐づける形でinterfaceを拡張している。

interface ITemplateStore{
    get(key: "test"): new Chonmage.Compiled<IRenderingContext>(/*..*/);
}

これによって正しいキーでテンプレートを取得しているかをコンパイル時にチェックできるようになる。 また、renderメソッドに渡すデータが指定したinterfaceを満たしているかのチェックもできる。

出力する変数にstringかnumber型以外を渡すとコンパイルエラーにする

これには賛否両論だと思うけど、自分はデータを表示用に加工する処理をテンプレートのレンダリング時に行うのは良くないと思っている。(あと実装を楽にするためでもある…。)

Chonmageができないこと

  • テンプレートに渡すinterfaceに配列を除くジェネリクス型を定義できない
  • 変数名が存在しない場合は空文字列を出力する
  • ラムダ(mustacheの仕様にのっとってラムダをサポートすると動的にテンプレートを作って挿入できてしまうので諦めた。)
  • グローバル変数の参照
  • Partial (これは時間がいずれやりたい)

そのほか

  • TypeScriptが提供しているLanguageServiceはあくまでエディタ向けを想定して作られているようだ。そのせいか型情報を取得してもほぼ文字列として帰ってきてしまう(TypeScript.Services.MemberInfo)。なのでもっとリッチな型情報が欲しいならコンパイラソースコードからいろいろ引っ張ってくる必要がありそう。
  • なぜかclassのプロパティは型情報の取得(getTypeAtPosition)ができなかった。なぜ?
  • TypeScriptの型情報は実行時には失われてしまうので、_.templateみたいにその場でテンプレートをコンパイルをして出力するのは難しそう。
  • IntelliJのTypeScriptサポートはかなり残念。
  • Mustacheのサブセットではなく、Razor Syntaxのようなもっとプログラム側に寄った記法のほうがTypeScriptを活かせたかもしれない。
  • TypeScriptの勉強のために書いたけど、型空間と変数空間の違いとか外部モジュールと内部モジュールの使い分けとか、クラサバ両方で使うためのコードをどう書くかみたいな知見が足りていないのでTypeScriptリファレンスを読むことにする。

東京Node学園 11時限目に参加してきたメモ

これからのNodeの話をしよう

気になったところ

  • tracing API: プロファイラが面白くなるかもとのこと。
  • koa expressの作者が注力しているフレームワークだそう。これによってフレームワークの勢力図が大きく変わるかも、とあった。なんでもyieldでミドルウェアを呼び出したりなどexpressに比べてエレガントに記述できるとか。
  • realtimeライブラリ:Socket.IO 1.0は新しいPerl6であるという皮肉を紹介。乱立するrealtimeライブラリの現状がわかりやすく説明されていた。ライブラリに決定打がないときは博打をうつよりもひとつ上のレイヤーで吸収するという解決案が素晴らしかった。

browserifyことはじめ 〜その仕組みと活用〜

  • browserify vs Require.jsの比較軸としてpreproccesing vs client drivenと紹介していたのがかなりしっくりきた。
  • 前処理をStreamを使って柔軟に処理できるtransform機構について非常に詳しく紹介されていてわかりやすい。
  • 既存のプロジェクトを近代化するならdeglobalifyが便利そう。ただ(function(export){ export.hoge = ""})(window)だと動かないとかnpmとバッティングすると挙動がわかりづらいなどいくつか罠がある模様。
  • Grunt/gulpとのすみわけが難しいという問題。個人的にはAltJSの変換を除いて.js->.js以外は認めない方針のほうが運用しやすいように思った。

その他

上二つのセッション目当てに行ったのだけど、LT枠がどれも面白かった。勉強会の途中からビールが配られて驚いたけど、結果として謎のテンションで会場が盛り上がったのは良かったと思う。

browserifyがサバクラ両方で動作するコードのために作られたようだという話を聞いて、クライアントとサーバーで処理を分散させたりその上にtorrentとかbitcoin的な仕組みをのせたら面白うそうだなと思ったのが今回の感想です。 発表した皆様お疲れ様でした。

Bacon.jsで眺めるFunctional reactive programming

JavaScriptのFunctional reactive programming(FRP)ライブラリ「Baconjs」を使ってFRPの考え方を勉強してみたメモ。

なお、このメモはFRPの勉強のためにいくつかの資料をざっくりまとめただけなので、Bacon.jsの使い方は解説しない。
なのでBacon.jsの使い方を知りたいだけなら元記事を読んだほうが良い。

三行で

  1. FRPでは従来のプログラミングでは間接的にしか扱えなかった「徐々に変化する値」を第一級のオブジェクトとして扱うことができる。
  2. さらに関数型のcompositionalな性質が合わさって素敵。
  3. Bacon.jsは良いものだ。

従来のプログラミングでは時間とともに変化する値の扱いが難しい

例えば次のフォームの例を考えてみる。

<form>
  <input type="text" id="username" name="username" placeholder="ユーザー名"/>
  <input type="text" id="fullname" name="fullname" placehodler="フルネーム"/>
  <button id="registerButton" disabled="disabled">登録</button>
</form>

このフォームに登録ボタンを押すとユーザー名とフルネームを送信する処理を実装してみる。

registerButton.click(function(event) {
    event.preventDefault();
    var data = { username: usernameField.val(), fullname: fullnameField.val()};
    $.ajax({
        type: "post",
        url: "/register",
        data: JSON.stringify(data)
    });
})

楽勝だ。
しかしこれではちょっと実用に耐えない。もう少し機能を追加してみる。

  1. ユーザー名が空の場合は送信ボタンを無効化する
  2. フルネームが空の場合は送信ボタンを無効化する
  3. ユーザー名が一文字入力されるごとに重複した名前があるかどうかをチェックする
  4. ユーザー名が重複していれば送信ボタンを無効化する

↓実装後

var usernameAvailable, checkingAvailability;
var usernameField = $('#username');
var fullnameField = $('#fullname');
var registerButton = $('#registerButton');
usernameField.keyup(function(){
    $.ajax({ url : "/usernameavailable/" + username}).done(function(available) {
        usernameAvailable = available;
        updateButtonState();
    });
});

registerButton.click(function(){
    updateButtonState();
    var data = {username: usernameField.val()};
    $.ajax({
        type: "post",
        url: "/register",
        data: JSON.stringify(data)
    });
});

var updateButtonState = function(){
    setEnabled(registerButton, usernameavailable
                               && nonEmpty(usernameField.val())
                               && nonEmpty(fullnameField.val())
                               && !checkingAvailability);
};

たった2つの入力項目をもつフォームのはずが、やや複雑になってしまった。
この調子でさらに

  1. ajax通信中はインジケーターを表示する
  2. 登録処理中はボタンを無効化する

といった処理を追加していくと、誰も保守できないコードになっていく。

このコードを改善するにはどうすればいいのか。
たしかにMVCオブジェクト指向を使ってコードをある程度改善することはできる。しかし、複雑さの根本には時間の経過(例えばユーザー名が入力によって少しずつ変化していくこと)が第一級のオブジェクトとして扱えないことにある。オブジェクト指向では状態とその変更という手段では時間経過を間接的にしか表現できないことが複雑さの原因だ。

FRPは「時間の経過を表現するデータ型」を提供する

FRPは徐々に変化する値(上の例ではユーザーの入力によって変化していくテキストフィールドの値)を第一級のオブジェクトとして扱うことができる。

さきほどの例をFRPライブラリのBacon.jsを使って書き換えていく。
まず、「入力によって変化していくユーザー名」を表現すると次のようになる。

var username = $('#username').asEventStream("keyup").map(function(event) { return $(event.target).val(); }).toProperty("");

このような値をFRPでは「振る舞い」と呼ぶ。
例えばユーザー名が変化するごとに内容をconsoleに出力するコードはこんな感じだ。

username.onValue(function(value){console.log(value);});

これを実行するとこうなる。

h
he
hel
hell
hello
hellow
hellowo
hellowor
helloworl
helloworld 

この程度なら従来の方法を使って同じことができると考えるかもしれない。

$('#username').keyup(function(e){
  console.log($(this).val());
}

FRPの利点は振る舞いを自由に加工したり、貼りあわせたりして新しい振る舞いを定義できることにある。
例えばユーザー名が空ではないことを表す振る舞いは以下のコードで表現できる。

var nonEmpty = function(x){ return x.length > 0;};
var usernameEntered = username.map(nonEmpty);

イベントを柔軟に扱えるようになった。

  1. ユーザー名が空の場合は送信ボタンを無効化する
  2. フルネームが空の場合は送信ボタンを無効化する

を実装すると

//usernameEnteredとほとんど同じ
fullnameEntered = fullname.map(nonEmpty);
buttonEnabled = usernameEntered.and(fullnameEntered);

//小さな振る舞いを組み立てていくことで副作用の部分を一箇所にまとめることができる
buttonEnabled.onValue(function(enabled){
  $("registerButton").attr("disabled", !enabled);
});

複雑な条件を簡潔に表現できて良い

疲れてきたので適当になるけど、FRPではajaxも同じ枠組みで扱うことができる。

var availabilityResponse = username.changes().map(function(user) { return { url: "/usernameavailable/" + user };}).ajax();
var usernameAvailable = availabilityResponse.toProperty(true);

//updatebuttonstateと同等の処理が次のようにかける!
var buttonEnabled = usernameEntered.and(fullnameEnterd).and(usernameAvailable);

ミュータブルな状態がなく小さなものから組み立てているので最初の実装例に比べて簡潔で拡張性が高い。

Bacon.jsすごく良い

Bacon.jsはjQueryプラグインとしても提供されているので既存のプロジェクトに混ぜるのも楽そう。
TypeScriptと組み合わせるともっと幸せになれそう。

FRPのテストについてとか、もっと大規模なアプリではどう構造化するべきなのかについても調べて後で書きたい