FlowからTypeScriptに段階的に移行する

この記事は TypeScript Advent Calendar 2019 の 3日目の記事です。

私の所属するマネーフォワード クラウド経費ではフロントエンドの基盤整備が進んでいます。もともとFlowでの型チェックが入っていたJavaScriptソースコードをTypeScriptへ移行しましたので知見を共有します。この記事ではなぜTypeScriptへ移行するのかの理由などは紹介せず、段階的に移行する方法について紹介します。

なぜ段階的に移行する必要があったか

既存のJavaScriptソースコードの数がそれなりに多く、一気に移行し不具合が発生するリスクを抑えるため、少しずつ段階的に移行する戦法を取ることにしました。

% find client/ -name '*.js' | wc -l
384

% wc -l `find client/ -name '*.js'`
...
57075 total

JavaScriptのファイル数は384、行数は57075でした。コンポーネントのテスト自体はある程度書かれていて、テストが通り軽く打鍵テストをすれば問題なく移行できそうなのは幸いでした。

どうやって移行したか

TypeScriptに段階的に移行するということは、.js, jsxファイル(以下.jsx?)と.ts, .tsxファイル(以下.tsx?)を混在させるということです。.jsx?は既存の方法でビルドをしつつ、.tsx?はTypeScriptとしてビルドなどをするという拡張子別に処理を分ける戦法です。

以下について、拡張子別に処理を分ける方法を紹介します。

  • Webpackでのビルド
  • Jestによるテスト
  • ESLintでのlint

Webpackでのビルド

もともとBabelを使用していたので.jsx?はBabel(babel-loader)を使用し、.tsx?はts-loaderを利用してビルドする戦略をとりました。Babelの7から対応したTypeScriptのプリセットを使うと既存の.jsx?にまで影響が及んでしまうため、loaderを分けています。

webpack.config.jsのmodule設定は以下の感じです。

module.exports = {
  ...

  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
      },
      {
        test: /\.tsx?$/,
        exclude: /node_modules/,
        loader: 'ts-loader',
      },
    ],
  },

  ...
}

Jestによるテスト

テストランナーとしてJestを利用しており、.tsx?のテストはts-jestを利用することでテスト時にビルドと型チェックをしてくれます。ts-jestを使うにはJestのtransform設定に.tsx?はts-jestを使う設定を追加します。(下記はpackage.jsonにJestの設定を書いた例)

  "jest": {
    "transform": {
      "^.+\\.tsx?$": "ts-jest"
    },
    "moduleFileExtensions": [
      "ts",
      "tsx",
      "js",
      "jsx"
    ],
  },

しかし、上記の設定を追加すると.jsx?ファイルのテストがうまく動かなくなります。Jestはデフォルトではbabel-jestというプラグインを利用して.jsx?をビルドしています。transform設定に.jsx?はbabel-jestを使用する設定を追加すると動くようになります。babel-jestはJestに依存してインストールされているため、あらたにnpm installをする必要はありません。

  "jest": {
    "transform": {
      "^.+\\.tsx?$": "ts-jest",
      "^.+\\.jsx?$": "babel-jest"
    },
    "moduleFileExtensions": [
      "ts",
      "tsx",
      "js",
      "jsx"
    ],
  },

また、ts-jestにはJestのpreset設定用に3つのPresetが用意されていますts-jest/presets/js-with-babel のPresetを利用すると.tsx?はts-jest、.jsx?ばbabel-jestを使ってビルドしてくれるので、transform設定ではなくpreset設定でも同じことができます。

  "jest": {
    "preset": "ts-jest/presets/js-with-babel",
    "moduleFileExtensions": [
      "ts",
      "tsx",
      "js",
      "jsx"
    ],
  },

ESlintによるlint

ESlintを利用していますが、ESLintの実行時に設定ファイルの指定と拡張子の指定ができます。.tsx?用の.eslintrcと.jsx?の.eslintrcを別々に用意し、eslintコマンドを別々に実行することで別々にlintすることができます。

  "scripts": {
    "lint": "npm run lint:ts && npm run lint:js",
    "lint:ts": "eslint -c .eslintrc.ts.yml --ext .ts,.tsx src/",
    "lint:js": "eslint -c .eslintrc.js.yml --ext .js,.jsx src/",
  },

ただlintは最悪あとからでも修正できるため、移行した.tsx?だけlintをかけるとか、後で全ファイルまとめて対応するでもよいと思います。私はlintはあとでまとめて全ファイルにかけるようにし、まずは移行を優先で進めるようにしています。

あとは気合で少しずつ移行していく

FlowからTypeScriptへ移行する際のビルド、テスト、lintを拡張子別に分けて段階的に移行していく方法を紹介しましたが、ここからFlowで書かれているコードを正規表現などで置換していき、TypeScript導入により厳密になった型チェックで怒られる部分を修正していく作業が待ち受けます。移行の際にはniieani/typescript-vs-flowtypeなどを参考にしつつ移行を進めました。

2019年11月頭頃から移行を始め、実はこの記事を書くまでにはすべてTypeScriptへ移行してリリースまでしている予定でしたが、思ったより手こずりまだリリースまでこぎつけていません。引き続きがんばります。