中規模のReact/JavaScriptアプリケーションをTypeScriptに移行するための第一歩を踏み出せました
やりたいこと
中規模のReact/JavaScriptアプリケーションを徐々にTypeScriptに移行する。
2018年ぐらいからやらなければいけないと主張していましたが、納期を問題に先延ばしにされ続けて、ついに新体制で開始できるようになりました。
結構面倒なことになりそうだったので、作業メモを取ることにしました。
問題
fd '.jsx?$'|wc -l
によるとJavaScriptファイルは280個。
fd '.jsx?$'|xargs wc -l
によるとJavaScriptコードの総行数は49501です。
開発チームの人数が少ないので、一気に全部移行するのは現実的ではないですね。本当はもっと小さいうちにやって置きたかったのですが。今一気にやると他のバグ修正とコンフリクトしまくるでしょうし難しい。
徐々に移行することを試みます
というわけで、
tsconfigのallowJs
とcheckJs
を使って混在してチェックできるようにします。
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部分に
//
でコメントしないで
- 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
で誤魔化す必要があります。
うーんこれだけの量が追加されるのは嫌ですね、一時的にstrict
をfalse
にして後で有効化することでディレクティブの数を減らしますか。
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
が教えてくれることに頼っています。
- Reduxのstateに型をつけて自動的に修正されるものを修正する
styles
オブジェクトを消してstyled-componentsにするだけで修正できるものが多いので修正する@ts-expect-error
全部消して修正する- tsconfigを
"strict": true
にして出てくるエラーを修正する plugin:@typescript-eslint/recommended-requiring-type-checking
を復活させて修正する"skipLibCheck": true
を消す。
久々にメモリ128GBの恩恵を感じました

これからどうなるのか
大量の@ts-expect-error
が生まれて、未だに我々は型に守られてはいません。しかし危険に近づいたら知りやすくはなったでしょう。移行計画を完了させくだらないTypeError
(主にundefined
へのアクセス)を見ることが減ることを望みます。
そうなればバグの発生率が減ることはもちろん、まともにリファクタリングもしやすくなるし、ライブラリのバージョンも上げやすくなります。