レガシーな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を使って開発するという手も考えられる。