自分のペースに合わせ、JavaScriptプロジェクトをTypeScriptに移行する方法
読む所要時間: 24 分
誰もが経験したことがあるでしょう。JavaScriptの機能やバグに取り組み、ようやく完了したと思いコードを実行したところ、無常にも「undefined is not a function(undefinedは関数ではありません)」というエラーが表示された、のような事態を。私はJavaScriptが大好きですが、プロジェクトのコードベースが大きくなるにつれ、面倒な状況に陥ることが何度もあります。コードベースの大部分を簡単にリファクタリングできることをはじめ、前述の「undefined is not a function」や類似のバグを回避することなど、JavaScriptの型に対するアプローチには誰もが悩まされてきました。このような問題の解決を支援するツールの1つとして、最近人気を集めているのがTypeScriptです。
TypeScriptは、JavaScriptに型システムを導入します。コードに対する理解が深まり、開発フローを支援してくれます。しかし、誰もが認識しているものの口にしたがらない、重要な問題についても早めに説明しておきます。多くの開発者は、静的型付け言語のように、すべてに型を追加する必要がない方が嬉しいものです。そもそも静的型付け言語を選択する理由もそこにあるかもしれません。
TypeScriptに興味がなくとも、この記事を読んでみてください。TypeScriptに移行する予定がなくても、メリットがあるかもしれません。例えば、Webpackのようなプロジェクトは、JavaScriptのままでTypeScriptを使用することに成功しています。このことについては記事の後半で説明します。
この記事は、自分のペースに合わせ必要な分だけプロジェクトにTypeScriptを取り入れることを前提にしています。どの章の後でも、一度立ち止まり、実際に動くコードベースを入手し、開発者のエクスペリエンスを少し向上させることができるでしょう。
すでにTypeScriptを使用しているかもしれない
これまでにTypeScriptを1行も書いたことがないと考えている人でも、実際はTypeScriptを書いたことがあり、TypeScriptのツールも使用したことがある可能性は高いです。
まず覚えていただきたいのは、TypeScriptがJavaScriptのスーパーセットであるということです。つまり、有効なJavaScriptはすべて有効なTypeScriptであるということです。正常に動作するJavaScriptを書いた場合、有効なTypeScriptを書いたことになります。しかも、明示的に型を宣言する必要はありません。TypeScriptは常に可能な限り型を推測しようとします。
すべての有効なJavaScriptが有効なTypeScriptであることを踏まえると、TypeScriptを導入したツールを使用したことがある可能性も高いでしょう。Monacoエディタは、Visual Studio Code、CodeSandbox、Stackblitzなどのさまざまなツールを支えているエディタです。TypeScriptを採用し、優れたオートコンプリートやコードリファクタリングなどの機能を提供しています。つまり、このようなエディタのオートコンプリートやリファクタリング機能を使用したことがある人は、TypeScriptを使用していたということです。
VS CodeによるJavaScriptのペアプログラミング
実は、私はほとんどTypeScriptコンパイラやTypeScriptファイルを使用していません。代わりに、通常のJavaScriptパイプラインツールを使用しています。しかし、私とペアプログラミングをしてくれるエディタにおいて、今もあらゆる段階でTypeScriptの力を借りています。
VS Codeには、エディタ全体または特定のワークスペースやプロジェクトについてのみ有効にできる設定があり、JavaScriptのTypeScriptチェックを有効にすることができます。
プロジェクトに対してこの機能を有効にするには、プロジェクトのルートディレクトリに.vscode/settings.json
ファイルを作成し、その中に次の内容を記述します。
または、同じルールをグローバルエディタの設定に配置するか、設定UIを使用してワークスペースの設定を編集することもできます。
これで、完全にタイプセーフではない行を書いた場合、TypeScriptがエディタ内でそのコードをマークしてくれます。例えば、このコードでは問題が発生します。
これは、process.env.PORT
がstring
型であり、また、3000
がnumber
型であるためです。これは、PORT += 1
が2つの異なる型の結果において等しくなることを意味します。最初のケースでは、文字列に「1」
が追加され、2番目のケースでは、数字の3001
という結果になります。
注意する必要があるのは、これらのエラーメッセージはエディタにしか表示されないため、コードに影響を与えないということです。このようなエラーが発生しても、コードは実行されるかもしれません。しかし同時に、テスト中に熟慮してこなかったシナリオについては警告を受けたことになります。
この設定は、意図的にある型のルールに違反している場合には、厄介に感じられるかもしれません。その場合、この設定が気に入らなければ、この設定を無効にするか、行の上に次のコメントを追加することにより、行ごとの型チェックを無効にすることができます。
全体の型チェックを無効にするには、次を使用できます。
個人的にはこの設定が好きで常にオンにしています。自分が書くJavaScriptのすべての行について、考えたこともないようなエッジケースがあるかもしれないと、いつも考えさせられるからです。常に見張り、助けてくれるペアプログラミングの相棒のようなものです。
JavaScript用TypeScriptコンパイラの使用
VS Codeにおいて有効にした設定は、実際にはcheckJs
というTypeScriptコンパイラの設定をセットするものです。この設定は、TypeScriptパーサーを通じてJavaScriptファイルを実行するallowJs
という別の設定に関連しており、これらを組み合わせれば、既存のJavaScriptプロジェクトを段階的にTypeScriptに移行するスタートとしては完璧と言えます。
その仕組みを説明するため、次のプロジェクトを例として説明します。github.com/dkundel/ts-move-demo
プロジェクトのクローンを作成したり、自分のプロジェクトに手順を適用したりして、自由に活用してください。
TypeScriptコンパイラのインストール
まず、TypeScriptコンパイラをインストールする必要があります。コンパイラはnpmにtypescript
として公開されており、tsc
というコマンドラインユーティリティを提供しています。コンパイラはグローバルにインストールすることもできますが、プロジェクトごとにdevの依存関係としてインストールすることにより、プロジェクトごとに異なるバージョンのコンパイラを使用できます。
プロジェクトのTypeScriptコンパイラをローカルにインストールするには、以下を実行します。
また、package.json
のscripts
セクションに新しいbuild
スクリプトをセットアップすることもできます。
これでビルドしたいときはいつでもnpm run build
を実行できます。また、ウォッチモードでTypeScriptを実行したい場合、npm run build -- --watch
を実行するか、tsc --watch
を呼び出す別のスクリプトを定義できます。
TypeScript構成の作成
たくさんの構成オプションをコマンドライン引数を介してTypeScriptコンパイラに渡したくない場合は、プロジェクトにtsconfig.json
ファイルを作成します。これには2つの方法があります。手動でファイルを作成する方法と、tsc --init
を実行し、デフォルトの設定を作成する方法です。このケースでは、tsc
を呼び出すbuild
スクリプトが設定されているため、次を実行することができます。
その後、プロジェクト内にtsconfig.json
が作成されます。これには利用可能なすべての設定と、それらの役割を説明するコメントが含まれています。コメントアウトされているものもあれば、TypeScriptがベストプラクティスとみなすものに設定されているものもあります。
これらの設定は新しいプロジェクトには適していますが、今回のユースケースには最適ではありません。
JavaScript用TypeScriptコンパイラの構成
最初のステップとして、TypeScriptコンパイラがJavaScriptファイルを読み込むようにします。
既存のtsconfig.json
を次のように置き換えます。
この構成では、デフォルトの設定から5項目を変更しました。
"target": "esnext"
:esnext
に変更しました。これは、TypeScriptがモダンJavaScript機能の下位トランスパイルを実行しないことを意味します。これにより、Babelのようなツールを使用したトランスパイルを続けることができます。値をes5
などに変更し、コードがどのように変化するか自由にテストしてみてください"allowJs": true
: TypeScriptコンパイラが検出したあらゆるJavaScriptファイルを読み込み、TypeScriptコンパイラを通じて実行することを意味します"checkJs": false
: ここではJavaScriptファイルの型チェックを無効にし、コンパイラに正しく渡されているかどうかだけを確認します"outDir": "./dist"
: コンパイラが読み込んだすべてのファイルが処理された後、rootDirの位置に基づいて新しいdist/
ディレクトリに配置されます。"rootDir": "./src"
: 一部のユースケースでは一般的にこの設定を省略できますが、特にJSファイルとTSファイルの両方を含む段階的な移行の場合、すべてのコードをsrc
、lib
などコードのルートとしたい任意のディレクトリに配置するようにします。これにより、コンパイラはすべてのコードを簡単に見つけることができ、dist
フォルダ内で構造を再現する方法を認識します。"strict": false
: TypeScriptに拒否されることなく、プロジェクトをTypeScriptに首尾よく移行させるために、ここではstrictモードを無効にします。
今回のプロジェクトでは、すべてのファイルがすでにsrc/
フォルダに移動されていますが、別のプロジェクトで試す場合、または別のフォルダを使用する場合は、ファイルを移動するか、rootDir
の値を調整する必要があります。
また、ソースファイルを参照するパスを、新しいコンパイル済みファイルに対応させる必要もあります。この例では、package.json
ファイルのmain
エントリを更新します。
設定がすべて完了したら、変更内容をテストしましょう。次のコマンドを実行します。
すべてのJavaScriptファイルを含む新しいdist
フォルダが作成されます。target
値を変更した場合はトランスパイルされる可能性があります。
その後コードを実行し、これまと同じく動作しているかどうかを確認できます。この例では、次のコマンド実行します。
そして、http://localhost:3000を開き、サーバーが稼働しているかどうか確認します。
TypeScriptを使用したJavaScriptのチェック
すべてのJavaScriptがTypeScriptコンパイラを通過するようになりました。次にJavaScriptの型チェックを有効にします。そのためには、tsconfig.json
のフラグを更新します。
Node.jsやElectronなど、DOMに属さないグローバルオブジェクトを使用している場合は、このステップで必要なtypesをインストールする必要があります。Node.jsの場合、次を実行します。
typesをインストールし、続いてコードをコンパイルします。今回はいくつかの型エラーが発生する可能性があるため、TypeScriptコンパイラをwatch
モードで実行し、ファイルを修正している間継続的に再実行されるようにします。
TypeScriptは、JavaScriptのコードに対する型を可能な限り推測しますが、JSDocコメントも考慮します。
VS Codeを使用し、checkJs
フラグを有効にしてプロジェクトを記述した場合、ほとんど修正はないでしょう。このステップで発生するほとんどのエラーは、アクセスする前にdocument.getElementById()
の結果がnull
でないかの確認など、チェック漏れに関連するものでしょう。または、推測された型に基づいて実際に発生するエラーかもしれません。
いずれの場合も、これまでに考えもしなかったようなものが見つかるかもしれません。また、意図的なコードについては、コードの行の上に// @ts-ignore
コメントをつけると、TypeScriptはそのコードを無視します。
隠れたバグの発見
この設定を行い、コンパイラが検出したエラーを修正したら、この時点で停止することもできます。これはWebpackが実際に使用しているアプローチです。WebpackはJavaScriptを使用して書かれていますが、エラーのチェックにはTypeScriptを使用します。2018年4月にWebpackがTypeScriptを導入したとき、コンパイラによりさまざまなバグを発見できました。コードベースがすべて徹底的にテストされていたにもかかわらずです。
Webpackのように、コードをトランスパイルすることなくTypeScriptを使用したい場合は、次のように構成を更新することができます。
この場合、コンパイラが追加のリンターのように動作します。
JavaScriptファイルをTypeScriptファイルに変換
ファイルがJavaScriptにおいて可能な限りタイプセーフであることを確認したら、次のステップでは、それらのファイルをゆっくりと1つずつTypeScriptファイルに変換していきます。すでにallowJs
を有効にしているため、一度にすべてのファイルを移動する必要はなく、徐々に移行していくことができます。
JavaScriptコードはすでに型チェックを実行したため、更新が必要な部分はあまりありません。
最初のステップは、ファイル拡張子を.js
から.ts
に変更することです。これにより、TypeScriptコンパイラにファイルがJavaScriptではなくTypeScriptであることを知らせ、型の定義など追加の構文機能が有効になります。
最も更新が必要になりそうなのは、値のimport
とexport
の方法です。すでにES Modules構文を使用している場合、ここでの作業は必要ありません。CommonJS構文を使用(基本的にrequire()
とmodule.exports
を使用)している場合、TypeScriptはES Modules構文を使用するため、更新が必要となります。
それぞれのrequire()
呼び出しを同等のimport
構文に置き換え、すべてのexports.*
をexport
に変更し、module.exports
を排除します。
src/routes/data.js
ファイルがsrc/routes/data.ts
に変換されると、次のようになります。
重要な注意点は、module.exportsはES Modulesの
export default
とは異なるということです。同等のものはありません。export default x
は、実際にはexports.default = x
とより同等ですが、esModuleInterop
オプションをtsconfig.json
に設定しているため、module.exports
を使用してJavaScriptファイルをimport
しようとすると、TypeScriptが処理します。
しかし、TypeScriptを使用して書いたファイルをJavaScriptから要求する場合、これでは動作しません。つまり、このケースでは、インポートファイルを次のように更新する必要があります。
一般的な経験則として、module.exports = x
を使用してデフォルトのエクスポートのように動作させることは回避すべきです。代わりに、各プロパティを個別にエクスポートするか、module.exports = { default: x }
と設定し、ES Moduleとの互換性を保ちます。
すべてのインポートを修正した後、TypeScriptコンパイラを再実行し、正常にパスするかどうかを確認します。その後にアプリを再実行し、すべてが適切に動作するかを確認します。確認できたら、すべてのファイルに対して同じプロセスを徐々に繰り返し、プロジェクトをTypeScriptに移行していきます。
厳密化: 型の追加
ファイルは、TypeScriptに変わりました。次はTypeScriptに情報を提供し、TypeScriptの力を発揮させましょう。これまでのところ、JSDocコメントを使用していない限り、何をしようとしているかについて、実際にはTypeScriptにあまり伝えていません。
TypeScriptには、型チェックをより厳密にするためのオプションがいくつかあります。それらは次のとおりです。
TypeScriptに徐々に取り組みたい場合、1つずつ有効にしていきます。すべての問題を一度に解決したい場合は、代わりにtsconfig.json
で次のオプションを設定することができます。
これで設定をすべて一度に有効にできます。
エラーの中には一部のライブラリに型が不足しているというものがあります。Node.js用の型をインストールする必要があったのと同じように、これらライブラリ用の型をインストールする必要があります。多くのライブラリに型が付属するようになったため、すべてのライブラリに対してこの作業を行う必要はありません。しかし、JavaScriptを使用して書かれたライブラリの中には、自分で型を維持していないものもあります。代わりに、DefinitelyTypedプロジェクト下でコミュニティにより維持されている型に頼る必要があります。例えばExpressJSの場合、次を実行してコミュニティの型をインストールする必要があります。
その型を自分のプロジェクト内でのみ使用し、独自の型を入れたライブラリのオーサリングをしてはいない場合、すべての型をdev dependenciesとしてインストールする必要があります。もう1つのケースについては後で説明します。
さまざまなstrictフラグを有効にした場合、表示されるエラーを必ず修正します。
その後、オプションでコードに型を追加し、さらに強力な型付けをすることができます。この例では、データルートがペイロードデータとして返せるものをより明確にしたいと思います。そのために、Payload
という新しいtype
を定義し、追加のヘルパー型をインポートして、src/routes/data.ts
ファイルの型を更新します。
この変更で{ "hello": "world" }
を持つオブジェクトのみを送り返すことができるようになります。他の文字列に変更したり、他のプロパティを追加したりしようとすると、拒否されます。その後、type Payload
の定義を変更し、ペイロードを調整することができますが、ここでは他の人が参照できる、チェックが容易なコントラクトを持つことになります。
コードベースがTypeScriptになるにつれ、より多くの型を追加することになるでしょう。型の定義に役立つさまざまなユーティリティ機能について、TypeScriptハンドブックやtype-festなどのライブラリで学習することができます。
TypeScriptによるライブラリの維持
社内外を問わず、他者と共有するライブラリを維持している場合、ビルドチェーンにTypeScriptを含めることを検討してもよい理由がもう1つあります。その理由は、型宣言ファイル(別名: TypeScript定義ファイル)です。これは、他者がそのライブラリをTypeScriptにより使用する際に使用されるファイルです。Visual Studio Codeなどのエディタは宣言ファイルを使用し、ライブラリのオートコンプリート情報を取得します。
これらの宣言ファイルは、2つの方法で配布することができます。1つは、ライブラリと一緒に直接配布する方法です。つまり、公開されたプロジェクトのどこかに(理想的にはトランスパイルされたJavaScriptと並べて)配置し、package.json
のtypes
プロパティにより、main
のJavaScriptファイルに対応する正しいファイルを指定します。これについては、TypeScriptドキュメントで詳しく説明されています。
これらの宣言ファイルは、tsconfig.json
のdeclarations
プロパティをtrueに設定することにより作成できます。
上記を設定すると宣言ファイルがJavaScriptと一緒に直接生成されます。TypeScriptバージョン3.7からは、allowJs
とdeclaration
を組み合わせることができるようになりました。つまり、プロジェクトがJavaScriptだけで書かれていても、手作業で維持する必要なく、基本的なTypeScript宣言ファイルを生成することができるようになりました。
他者がライブラリの型を使用する別の方法は、コミュニティプロジェクトであるDefinitelyTypedを利用する方法です。誰でもプルリクエストを作成し、宣言ファイルを追加・更新し、宣言を自己公開しないライブラリとすることができます。
大切なことは、TypeScriptというツールを活用できるかは、あなた次第ということです。使うのも使わないのも自由です。しかし、良い友人関係と同じように、ただそれを利用するのではなく、自分から何かを投入すると、より良いものになります。TypeScriptは、コードの型を可能な限り推測してくれます(そして常に改善されていきます)。しかし、TypeScriptにさらなる力を発揮してもらいたいと思うなら、時には自分からアシストする必要があります。
これらのステップがTypeScript使いこなすための助けとなり、TypeScriptがプロジェクトの成長に役立つことを願います。私は個人的に、これらのステップを使用して、twilio-run
などのプロジェクトをJavaScriptからTypeScriptに移行できました。デモプロジェクトに適用されたステップを確認したい場合、関連するプルリクエストをチェックしてください。
ご質問があれば、お気軽にお問い合わせください。喜んでお手伝いします。
- Email: dkundel@twilio.com
- Twitter: @dkundel
- GitHub: dkundel
- dkundel.com