• 作成:
  • 更新:

中規模のReact/JavaScriptアプリケーションをTypeScriptに移行するための第一歩を踏み出せました

やりたいこと

中規模のReact/JavaScriptアプリケーションを徐々にTypeScriptに移行する。

2018年ぐらいからやらなければいけないと主張していましたが、納期を問題に先延ばしにされ続けて、ついに新体制で開始できるようになりました。

結構面倒なことになりそうだったので、作業メモを取ることにしました。

問題

fd '.jsx?$'|wc -lによるとJavaScriptファイルは280個。 fd '.jsx?$'|xargs wc -lによるとJavaScriptコードの総行数は49501です。

開発チームの人数が少ないので、一気に全部移行するのは現実的ではないですね。本当はもっと小さいうちにやって置きたかったのですが。今一気にやると他のバグ修正とコンフリクトしまくるでしょうし難しい。

徐々に移行することを試みます

というわけで、 tsconfigのallowJscheckJsを使って混在してチェックできるようにします。

JavaScriptもチェックしたいが現在のコードは悲惨

最終的には当然"strict": trueでチェックするのですが、全部JavaScriptで書かれてたことを考えるとそのままチェックするとanyなどの警告が出まくります。

これを全部修正するならばTypeScriptに全部修正するほうがまだマシですね。かと言って、 "noImplicitAny": false をするのはせっかくのTypeScriptの利点を殺してしまいます。

つまり私が求めるのは、 TypeScriptコードでは普通の挙動をして、 JavaScriptコードでは既にTypeScriptになってるライブラリなどの型情報と照らし合わせて明らかに間違っているものはエラーにするが、まだ型のついてなかったりするものやnullかもしれないものは見逃してTypeScriptにする時に修正するような形がほしい。

allowJsでどうこうするよりts-migrateの方が良いのでは

そういう複雑なconfigを書くより、 airbnb/ts-migrate: A tool to help migrate JavaScript code quickly and conveniently to TypeScript で一気に変換してコメントでエラーを無視して、一つずつエラー無視を消していく方が良いのではと思えてきました。

混在ビルドは面倒なことが他のメンバーの努力で分かってきたので。

一度試してみます。

ts-migrateを試してみます

ts-migrate/packages/ts-migrate-plugins at master · airbnb/ts-migrate とかどうすれば実行されるんだろうと思ってたけれど自動的に実行されるっぽい。むしろ無効化するのに引数が必要?

webpackとかの変換もしても悪くはないけれど、 .eslintrc.tsとか無いっぽいしまずは本体ソースだけから。

実行。

npx -p ts-migrate -c "ts-migrate-full src"

エラーを得る。

/home/ncaq/.npm/_npx/192e513e19957e74/node_modules/.bin/ts-migrate-full: 行 67: ./node_modules/ts-migrate/build/cli.js: そのようなファイルやディレクトリはありません

はて。

npx ts-migrate-full src

の方も404だし… npxのくせにインストールしないと実行できないのかこれ。一回インストールして実行して消すのが必要なんですね。

インストールして実行したらsrc以下に新規にtsconfig.jsonと.eslintrcを作ってコミットしようとして、自分のコミット規約に引っかかって失敗している。 tsconfig.jsonはルートにあるんですよね。統合テストコードとかは独立しているのでTypeScriptで先に書いていました。

ああ処理フォルダとプロジェクトルートを指定する必要があるのか?

yarn ts-migrate-full . src

これでプロジェクトルートは認識したみたいですが、 tsconfig.jsonは作ろうとするわ、コミットはしようとするわで失敗。

fullでやろうとするのが悪くて、 initをスキップすれば良い?

yarn ts-migrate rename . src

あれ引数2つで処理フォルダ指定とか思ってたけどルートのwebpackとかも変換されるなあ。

npx ts-migrate-full <folder> /                # specify the project root, and
  --sources="relative/path/to/subset/**/*" /  # list the subset to migrate,
  --sources="node_modules/**/*.d.ts"          # including any global types that the
                                              # migrator may need to know about.

あっ公式ドキュメントのスラッシュ、これ改行区切りたいだけで引数ではなかった。 --sourcesで指定するのね。

yarn ts-migrate rename . --sources=src

Gitのファイルトラッキングに優しくするためにrenameの段階でコミットしましょう。

単体testでjsのやつもまだあるのでそれもrename。

それで次は本番の移行。

yarn ts-migrate migrate . --sources=src

??? 何も起きないんですけど?

どうもmigrateコマンドの場合--sourcesに渡すのはディレクトリだとダメらしい。 renameの方は良かった理由がよくわからない…

yarn ts-migrate migrate . --sources "src/**/*"
RangeError: Maximum call stack size exceeded

になってしまった。 webpackでバンドルされる画像や音声ファイルとかも含んでたからかな。最初はシェルで展開して渡したけれどそれだと一つしか処理されなかったんですよねえ。

じゃあこれで。

yarn ts-migrate migrate . --sources "src/**/*.{ts,tsx}"

一応これで処理は通りましたね。

ts-migrateの問題

ただ問題が多い。

  • ぶっ壊れているASTが多い
    • JSX部分に//でコメントしないで
  • @ts-expect-errorで示される型エラー警告に括弧が大量に含まれるせいでEmacsの括弧合わせシンタックスが崩壊する
  • ESLintの警告がドシドシ出る
  • prop-typesは変換されない
  • 本来推論できるレベルのはずの引数などにanyが多すぎる
    • TypeScriptに期待しすぎ?

こういうバグった出力は結構普通にあるらしい。 JS→ts-migrateでTypeScript化→ESLint導入 エラー対処メモ気合でどうにかする必要がある?

流石にJSX部分ほとんど全部壊れるとなるとReact向けじゃないのではとしか思えないですね… いやなんか昔は問題なかったけどTypeScriptコンパイラのバージョンアップとかで壊れたらしい。 ts-migrate is inserting comments in JSX components · Issue #150 · airbnb/ts-migrate 一応TypeScriptコンパイラをダウングレードすれば動くらしい。

うーん。今回は殆どJavaScriptからの変換なので、殆ど全てにおいてTypeScriptとして型の整合性が壊れているんですよね。全てのコードに一度型チェックエラー無視をつけても良い気がしてきました。そっちの方がスッキリしますし。一部だけ壊れているとかならともかく、 Reduxのstate全体に型がついてないのでだいたいのコードが壊れているため。

ts-migrateに頼らない前処理

prop-typesに関しては先にこちらを使ってみましょう。 mskelton/ratchet: Codemod to convert React PropTypes to TypeScript types. ちゃんと消えました。 React 17に対応してないのでReact.ReactElement前提で書き換えられるとかはありましたが、全体置換で書き換えられるレベル。

それで、 TypeScript化して不要になりそうなパッケージは消してから、 jeffijoe/typesync: Install missing TypeScript typings for dependencies in your package.json. を使って自動的に型がつくライブラリにはそうしてもらった方が良いでしょう。

後で気がついたんですが、 typesyncは割と漏れが発生するので過信しない方が良いですね。

ESLintに関してもしばらくファイル全体で型エラーに関する警告は消しておいても良いかもしれません。

webpackとかちょっとしたスクリプトもts化してしまいましょう

ESLintを基本的にTypeScript前提にしたのでエラーが出るのが鬱陶しいため。

WebpackのconfigファイルをTypeScriptで書こうとするとdevserverプロパティでTSエラーになる| Shun Bibo Roku は今は普通に解決していました。

@types/webpack-dev-server入れて、

import 'webpack-dev-server';

って書くだけ。

もうみんなwebpackとか使ってないか… 一気にViteに移行したほうが良かったのかなあ。

ts-loader入れると遅いし。まあこれは型チェックも入れてるからなんですが… ts-loaderの型チェックは切って、 CIのtscに期待しましょう。将来的にはViteあたりに移行します。

afterSignはts-nodeで実行するの難しかったのでこだわらずにJavaScriptで諦めました。

ts-migrate-pluginsのうち使えるものだけを選定する

ts-migrate/packages/ts-migrate-plugins at master · airbnb/ts-migrate が結局ASTを大破壊するので殆ど使えないことが分かってきました。

使えるものだけを使いましょう。

add-conversions, explicit-any

anyが自動的についても嬉しいことは何もないです。

Reduxのstateとかに将来的に型がちゃんとついたら自動的に推論出来るものも増えるはずなので、 anyを明示的につけて嬉しいことはないです。

declare-missing-class-properties

なんすかこれ、使ってみたらclassのプロパティにanyがつきました。いらない。

eslint-fix

ESLintを直接動かせば良いのでいらない。

hoist-class-statics

動かしてもなんもありませんでした。

jsdoc

そこそこ書いてたつもりで、 lspとかは認識してるので間違ってるとは思わないんですが、なんか全く変換しませんでした。なんで?

member-accessibility

今回はライブラリ書いてるわけじゃないんで可視性とかどうでも良いです。

react-class-lifecycle-methods

prop-types変換済みだからか何も起きませんでした。

react-class-state

変更なし。

react-default-props, react-props, react-shape

prop-typesは他のツールでより良く処理済みなので不要。

ts-ignore

ASTを破壊するから使えない。

strip-ts-ignore

なぜかと言うとReduxのstateにちゃんと型がつけばエラーなしにTypeScriptになるものは多数存在するはずだから使えるかと思いきや、よく考えてみると@ts-expect-errorを使っていればtscが検出するから不要ですね。

ts-migrateあんまり役に立ちませんでした

うーん鳴り物入りのツールだったはずなんですが。元のJavaScriptのコードの品質がよほど高ければ、型エラーになるのはごく一部になるから意味があるのかもしれません。 @ts-checkを使っているとか。

最初に考えていた真面目にTypeScriptにするモジュールは変換して、まだ変換できないものはチェックをかなり弱めてJavaScriptのままにする方針の方が良かったかもしれません。

きちんとした型付けがされたTypeScript化がどこまで進んでいるのかがわかりにくくなる

100万行の大規模なJavaScript製システムをTypeScriptに移行するためにやったこと | CyberAgent Developers Blog

まあしかし我々は大規模ではなく中規模なので、私がコードレビューで見ていれば段々と@ts-expect-errorが無効になってくれるコードは増えるかもしれない。期待したい。

ところで無効になった@ts-expect-errorをワンコマンドで消す方法はない感じですかね? 将来的に大量に消す必要が出てくるのですが… まあ正規表現で雑に消してprettierかければ良いか。

jsdoc書いてた部分で一切変換が無いのは流石におかしいのでは

ちゃんとlspには認識されているのに… 変換ツールいくつか眺めてましたが引っかかりませんでしたね。まあそんなに書いてなかったから良いか…

@ts-expect-errorを付与

@ts-expect-errorを自動追加!suppress-ts-errorsの紹介の方を使うのが良さそうかなと思いましたが、あまりにも付与位置が多すぎるので、ファイル全体に付与するだけで済ませることにしたい。

Redux使ってるやつ全部影響するから流石にねえ。全体でanyを許可不許可するのも影響が広すぎそうですし。

というか問題はanyではなくgetState{}と推論されることにあるので、対処方法は// @ts-expect-error 2339であって、 anyへの対処はダメですね。

と思ったら@ts-expect-errorは行単位だけでファイル単位は@ts-nocheckと違って無いんですね。これはsuppress-ts-errorsを使うしかありませんね。行数を大量に増やすコミットをするのはすごい嫌なんですが。仕方ない。

一部Unused '@ts-expect-error' directive.とか出ます。付与する基準がおかしい? 大抵はライブラリの問題とunusedなので機械的な対処でどうにかなる気がします。

styled-componentsの破壊的変更が怖いが型エラーを修正するには上げるしか無かった。

一部prettierがディレクティブの対応をぶち壊すので、 prettier-ignoreで誤魔化す必要があります。

うーんこれだけの量が追加されるのは嫌ですね、一時的にstrictfalseにして後で有効化することでディレクティブの数を減らしますか。

reducerをいじってgetStateの結果をanyにする

getStateの結果が初期値の{}になってしまうとstrict無効でも雑にプログラミングするのが難しすぎるので、 reducerの生成で初期値を{} as anyにでもして、 strictを切って@ts-expect-errorの数を減らすと良いです。

後からそれに気がついたので無駄な改行が大量発生して修正が大変になってしまった。つらい。無駄な改行を削除してもう一度実行したら、

Error: targetNode is not found at line 359
    at isSomKindOfJsxAtLine (/home/ncaq/.yarn/berry/cache/suppress-ts-errors-npm-1.2.0-f93b279837-8.zip/node_modules/suppress-ts-errors/dist/lib/buildComment.js:15:15)

とか言われて進まなくなってしまった。

rebaseします。

ESLintのルールを弱める

plugin:@typescript-eslint/recommended-requiring-type-checkingが入ると、 tscのエラーを抑制しても問答無用で多数発動するから一時的に弱めるしかなさそう。いつか帰って来てください。

flycheckってエラー表示制限あるんですね

明らかにanyだらけだろって場所でもエラーにならないからおかしいなあと思ったのですが、

Warning (flycheck): Syntax checker lsp reported too many errors (495) and is disabled.
Use M-x customize-variable RET flycheck-checker-error-threshold to
change the threshold or M-x universal-argument C-c ! x to re-enable the checker. Disable showing Disable logging

のようにあまりにもエラーが多いとflycheckは表示を放棄するんですね、知りませんでした。

将来的なエラー無効化の無効化計画

修正されたエラーは@ts-expect-errorが教えてくれることに頼っています。

  1. Reduxのstateに型をつけて自動的に修正されるものを修正する
  2. stylesオブジェクトを消してstyled-componentsにするだけで修正できるものが多いので修正する
  3. @ts-expect-error全部消して修正する
  4. tsconfigを"strict": trueにして出てくるエラーを修正する
  5. plugin:@typescript-eslint/recommended-requiring-type-checkingを復活させて修正する
  6. "skipLibCheck": trueを消す。

久々にメモリ128GBの恩恵を感じました

使用メモリ

これからどうなるのか

大量の@ts-expect-errorが生まれて、未だに我々は型に守られてはいません。しかし危険に近づいたら知りやすくはなったでしょう。移行計画を完了させくだらないTypeError(主にundefinedへのアクセス)を見ることが減ることを望みます。

そうなればバグの発生率が減ることはもちろん、まともにリファクタリングもしやすくなるし、ライブラリのバージョンも上げやすくなります。