Alminというライブラリは元々JavaScript(+Babel)で書かれていましたが、今年の2月にsrc/下のソースコードはTypeScriptに移行しました。

その時のコミットログは次のPRに残っているため、コミットログを1コづつ見ていけばどのように行われていったが分かると思います。

コミットログ

この時取った方法は大まかに次のような手順でした

src/ の TypeScript化

  1. Babel -> JS(js -> js)だったものをTypeScript -> Babel -> JSにビルドスクリプトを変更
    • TypeScriptはtargetesnextにすることで単純に型を取り除くだけの変換にする
    • ES2015 -> ES5を実際にやるのは既存のBabelのまま
    • 空のtsファイルを一つおいて実際にコンパイルが通るかを検証
  2. 既存のテストを1で変換したソースで動くようにパスを変える
  3. 1つづつ.js.ts変換していく

という手順でJavaScript to TypeScriptを行いました。 この時の変更では、test/下のテストファイルはJavaScriptのままでした。

テストの方も複雑なケースが色々増えてきたので、TypeScriptに移行したいなーと思って最近移行できる環境を作りました。

test/ のTypeScript化

src/下はまとめて移行するスタイルでしたが、test/下は徐々に必要なタイミングで移行できるような形にしています。

テストコードはsrc/下とは違い、テストコード同士が互いに依存することはないため、1つのテストファイルごとに移行していけるはずです。

そのため、src/のやり方とは異なりTypeScriptのallowJs機能を使って移行しています。 次のPRで移行した手順が見られます(試行錯誤したのでキレイなコミットではない)

実際にまとめると次のような方法で移行しています。

  1. test/以下にテスト用のtsconfig.jsonを追加する
    • src/用のコンパイル設定を"extends": "../tsconfig.json"で継承し、"allowJs": trueの設定を追加したものを利用
    • "allowJs": trueにすることで.jsもTypeScriptがコンパイルできるようになる
    • src/**/*test/**/*をincludesし、テスト向けにテストコードとソースコードを一緒にコンパイルする
    • コンパイル結果、 srclib/srcへ、 test/lib/testへ出力する
  2. テストファイルがimport {Store} from "../lib/"のソースコードをみているのを、import {Store} from "../src/"のソースコードを見るようにする
    • コンパイル前はsrc/*.tsを見るが、コンパイル後はlib/src/*.jsを見ることになる
    • 出力先はlib/{src,test}/となっていて、lib/testから../srcを見るとlib/srcをみる状態を作れる
    • これでコンパイル結果は全てただのJSなので、後は普通にmochaなどでJavaScriptのテストとしてlib/testを実行する
  3. テストの実行前にtest/tsconfig.jsonを使ってコンパイルする
    • "test": "npm run build:test && npm run test:js"という感じ

test/tsconfig.json:

{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "allowJs": true,
    "declaration": false
  },
  "include": [
    "../src/**/*",
    "./**/*"
  ]
}

./tsconfig.json"rootDir": "./",を設定しておけば、テスト用と普段の設定で出力先が同じになる。

これで.ts.jsのテストファイルが混在している状態でもテストが実行できるので、必要なタイミングで.js.tsに変更していけます。

またすべてのテストがTypeScriptにはなってないのでコントリビュート待ってます。 基本的には.tsにして型エラーを潰していくだけの作業だと思います。

すべてのテストが.tsになった後は、ts-node/registerなどを使ってruntime hookで変換できるのでnpm run build:testが必要なくなります。

おわり

src/のように依存関係があるものは一気にTypeScriptへ変換した方が良いです。

一方、test/のようにそれぞれのファイルが独立しているものは、--allowJsを使うことで既存のJavaScriptを混ぜた状態でTypeScriptへ移行できるようになりました。

最近だと--checkJs--allowJsを使うことでJavaScriptファイルに対してJSDocを使ったType Checkができるようになっています。 これを上手く使えば、もっと緩やかに移行することができるかもしれません(JSDoc全部のパターン対応してないので今の所限定的)

今回はライブラリだったので、テストカバレッジがかなり高い状態でした。 なのでテストがある程度保証してくれるので、一気にやっても壊れにくい状態でした。

UIなどテストがしにくい部分を含む実際のアプリケーションに後から型付けしていく場合は、次の記事などが参考になるかもしれません。