Babelで書かれたJavaScriptライブラリをTypeScriptへ移行する方法
Babelを使ってJavaScriptで書いていたライブラリをTypeScriptへマイグレーションする方法についてのメモ書きです。 数十回はライブラリやアプリケーションのコードベースをJavaScriptからTypeScriptへ変換しているので、 ある程度やり方がパターン化されています。
この記事では、自分がよく利用している次の構成のライブラリを元にしています
- Babel 6 or 7
- Mocha + @babel/register
この構成を、次のようなTypeScriptを使った構成へと変換していきます。
- Babel -> TypeScript
- Mocha + ts-node + ts-node-test-register
Babelで書かれたライブラリをTypeScriptへ変換
今回はtextlint-rule-helperというライブラリを例にしていきます。 このライブラリは次の記事で、Babelを使ってライブラリを書く構成として紹介しています。
このBabelを使って書いたライブラリのソースコードをTypeScriptへ変換し、配布するところまでを見ていきます。
実際に変換したPull Requestは次のURLで見れます。
Babelでの構成
ざっくりとBabel版でのtextlint-rule-helperは次のような構成になっています。 (v2.0.1がBabelでのソースコードになっています。)

src/- ES2015で書いたソースコード
lib/- Babelで変換した結果(ES5)のソースコード
- 実際のライブラリとして配布するのはこちら
- 変換結果(自動生成)なのでGitの管理下には置かないように
.gitignoreで無視する - 一方で配布はするので
package.jsonのfilesフィールドでnpmにはpublishする
test/- ES2015で書かれたテストコード
- @babel/registerを利用して、実行時に変換しながらテストする
詳しい構成については次の記事で解説しているので合わせてみてください。
TypeScriptでの構成(予想図)
この記事の最終的な構造の予想図は次のようになっています。

src/- TypeScriptで書いたソースコード
lib/- TypeScriptで変換した結果(ES5)のソースコード
- 実際のライブラリとして配布するのはこちら
- 変換結果(自動生成)なのでGitの管理下には置かないように
.gitignoreで無視する - 一方で配布はするので
package.jsonのfilesフィールドでnpmにはpublishする
test/- TypeScriptで書かれたテストコード
- ts-nodeとts-node-test-registerを利用して、実行時に変換しながらテストする
変換するのがTypeScriptに変わった以外は、Babelの構造と何も変わっていないことがわかります。
Babel to TypeScript
実際にBabel to TypeScirptへの変換をしていきます。
次の記事でもJavaScriptからTypeScriptへの移行方法について書いています。
textlint-rule-helperはソースコードとテストコードどちらもTypeScriptにへ変換する予定です。 大まかなやり方は上記の記事と同じで、次のような流れでTypeScriptへ変換していきます。
- TypeScriptをインストールする
- TypeScript(tsc)でJavaScriptをビルドできるようにする
- TypeScript(tsc)でJavaScriptのテストを通るようにする
- ソースコード(src/)をTypeScriptへ変換する
- テストコードを(test/)をTypeScriptへ変換する
1. TypeScriptをインストールする
まずは、TypeScriptなどの必要な依存をまとめてインストールします。 ここでは、ts-nodeなどあとで必要になるものをまとめています。
npm install --save-dev \
typescript \
ts-node \
ts-node-test-register \
mocha \
@types/node \
@types/mocha
2. TypeScript(tsc)でJavaScriptをビルドできるようにする
ここでは、ソースコードをJavaScriptのままTypeScript(tscコマンド)でビルドできるようにします。
TypeScriptはallowJsというオプションによって、JavaScript(ES2015+)をJavaScript(ES5)へと変換するTranspilerとして利用できます。いままで、BabelでやっていたのはES2015 -> ES5の処理だったので、これをTypeScriptでやるように移行していきます
JavaScriptをビルドするtsconfig.jsonを作成
まずは、tsc --initコマンドで、tsconfig.jsonファイルを作成します。
既にtsc(TypeScriptコンパイラ)がnode_modules以下にインストールされていると思うので、tsc --initコマンドを叩くことでデフォルト設定のtsconfig.jsonを作成します。
./node_modules/.bin/tsc --init
# or
npx tsc --init
# or
yarn tsc --init
デフォルトのtsconfig.jsonは.tsファイルをビルドする設定になっているので、
JavaScriptファイルもビルドできるようにallowJsオプションを有効化します。
allowJsをtrueへdeclarationをfalseへallowJsが有効時はd.tsファイルを生成するdeclarationはfalseでないといけない- Allow
--declarationwith--allowJs· Issue #7546 · Microsoft/TypeScript
{
"compilerOptions": {
/* Basic Options */
"module": "commonjs",
"moduleResolution": "node",
"esModuleInterop": true,
"newLine": "LF",
"outDir": "./lib/",
"target": "es5",
"sourceMap": true,
// == 変更点 ==
"declaration": false,
"allowJs": true,
// == 変更点 ==
"jsx": "preserve",
"lib": [
"es2018",
"dom"
],
/* Strict Type-Checking Options */
"strict": true,
/* Additional Checks */
/* Report errors on unused locals. */
"noUnusedLocals": true,
/* Report errors on unused parameters. */
"noUnusedParameters": true,
/* Report error when not all code paths in function return a value. */
"noImplicitReturns": true,
/* Report errors for fallthrough cases in switch statement. */
"noFallthroughCasesInSwitch": true
},
"include": [
"src/**/*"
],
"exclude": [
".git",
"node_modules"
]
}
Tips: 最近の--initではesModuleInteropはtrueになっています。
このオプションは有効化した方がBabelのimportと挙動が近くなるのでtrueにしておいたほうがマイグレーションは簡単です。
tscでJavaScriptをビルド
次にnpm run buildでビルドできるように、package.jsonのscriptsを次のように変更します。
npm run buildでビルド、npm run watchでファイル監視とビルド、npm testでテストという感じです。
"files": [
"lib"
],
"main": "lib/index.js",
"types": "lib/index.d.ts",
"scripts": {
"build": "tsc -p .",
"prepublish": "npm run --if-present build",
"test": "mocha \"test/**/*.{js,ts}\"",
"watch": "tsc -p . --watch"
},
これで、npm run buildコマンドでsrc/以下にあるJavaScriptファイルをビルドしてlib/へ出力できるようになります。
Note: npm run buildが通らない場合
Babelで特殊な処理をしている場合はtscではビルドできない場合もあります。
(ソースコードにECMAScript外の記法が利用されていたり、import a from "~/hoge"のような拡張をしている場合)
textlint-rule-helperでは@babel/preset-envのみの利用となっています。
Tips: 手作業でscriptsを書き換えるのが面倒な場合はnpeコマンドを使うと、CLIで書き換えができるので簡単です。
npx npe scripts.build "tsc -p ."
npx npe scripts.watch "tsc -p . --watch"
npx npe scripts.prepublish "npm run --if-present build"
npx npe scripts.test "mocha \"test/**/*.{js,ts}\""
# types
declare currentDir=$(pwd)
declare currentDirName=$(basename "${currentDir}")
npx npe types "lib/${currentDirName}.d.ts"
これでソースコードをとりあえずtscでビルドできるようになりました。
ただし、型チェックなどもなにもしてないので、ビルドしたものが実行できるかはわかりません。
次は、既存のテストを同じようにallowJsで実行してテストを通るようにしていきます。
3. TypeScript(tsc)でJavaScriptのテストを通るようにする
元々をMocha + @babel/registerを利用していたのを、ts-nodeとts-node-test-registerへ移行します。
まずは、テストコード向けのtsconfig.jsonを作成します。
次のようにtest/tsconfig.jsonという場所に作成します。
わざわざ、テスト用のtsconfig.jsonを作成しているのは、設定を分けたほうが少しづつ移行しやすいのとテスト用に設定を分けたほうが柔軟性が高いためです。
test/
├── IgnoreNodeManager-test.js
├── mocha.opts
├── tsconfig.json
└── textlint-rule-helper-test.js
test/tsconfig.jsonには、先ほど作成したルートのtsconfig.jsonを継承して、次のような設定で上書きします。
declarationとallowJsはJavaScriptファイルを扱うためextendsしているので特に書く必要はないが、あとでルートのtsconfig.jsonからは削除するため重複して書いている
noEmitをtrueにしてビルドしてもファイルを出力しないようにするincludeに、テストファイル自身(./**/*)とソースコードのディレクトリを指定する
{
"extends": "../tsconfig.json",
"compilerOptions": {
"declaration": false,
"allowJs": true,
"noEmit": true
},
"include": [
"../src/**/*",
"./**/*"
]
}
そして、mocha.optsで読み込むregsiterを@babel/registerからts-node-test-registerへ変更します。
- --require @babel/register
+ --require ts-node-test-register
ts-nodeは実行時にTypeScriptを変換してくれるライブラリで、@babel/register相当の処理を行います。
ts-node-test-registerはts-nodeのラッパーで、test/tsconfig.jsonを自動的に読み込むようにしたテスト用のregisterです。(ts-nodeはパスを設定しないとルートのtsconfig.jsonを読み込みます)
そして、JavaScriptで書かれたテストをnpm testコマンドで実行します。
npm test
これでテストが通ってるならOKです。 この時点でプロジェクトからBabelの依存はなくなりました。
Notes: テストが通らない
ここでテストが通らないのは、テストコードにBabelに依存した処理があるのかもしれません。 Fixturesを動的に読み込んでいる場合などの挙動で違いが出るケースもあります。
また、JSDocのようなエラーメッセージがでているなら、checkJs: trueとなってるのかもしれません。
(JSDocの型チェックが行われていて、型が間違っていると通りません)
このサンプルであとでTypeScriptに変換するので、checkJs: falseでもいいかもしれません。
4. ソースコード(src/)をTypeScriptへ変換する
次に、やっとコードをTypeScript(.ts)へと変換していきます。
ここでやることはsrc/*.jsを一個ずつ.tsへと変換していくだけです。
基本的な考え方:
.tsへ拡張子を変更npm testが通るまで型をつけていく.jsと.tsは一応混在してても動くので、1つずつ.tsにしていく- 1ファイル変換できたらコミットする(巻き戻せるように)
変換していく順番の考え方:
- 依存の末端から
.tsにしていく- そのファイルが何も
importしていないファイルが一番最初 - 依存がないファイルは変換が簡単
- そのファイルが何も
- ライブラリは
@types/*があるかを確認する@types/をインストールするツールを使うのが簡単- nfour/types-installer: Installs @types for your dependencies
- xavdid/typed-install: Easily install new packages and their types, every time.
@typesがない場合はconst a = require("a")としてanyで扱うようにするのがシンプル
- JSDocが書かれているとWebStormやVScodeのQuickFixで推測してTypeScriptの型をつけてくれる
- ついてなくても
Infer typeから結構推測してつけたりできる - とりあえずテストコードの変更は最小で型をつけていけばいいので、
anyとかでもいいはず - 実際にちゃんとした型をつけるには、使う側(テストコードなど)をTypeScriptにしないと難しいので
- ついてなくても
- テストはできるだけ変更しない
- テストが既に動いてるはずなので、できるだけテストコード自体は変更しないようにしてテストを通す
- テストのロジックをうっかり壊さないようにする
Convert JavaScript to #TypeScript by #VSCode .
— azu (@azu_re) November 26, 2017
Refactoring JSDoc to TypeScript annotations is useful. pic.twitter.com/P8o1Tc2MUf

これでsrc/*.jsはsrc/*.tsに変換されました。
ルートのtsconfig.jsonからallowJsを外す
ソースコードは.tsになったため、ルートのtsconfig.jsonからallowJsを外してビルドできるかを確認します。
allowJsを消すdeclarationをtrueへ
{
"compilerOptions": {
/* Basic Options */
"module": "commonjs",
"moduleResolution": "node",
"esModuleInterop": true,
"newLine": "LF",
"outDir": "./lib/",
"target": "es5",
"sourceMap": true,
// == 変更点 ==
- "declaration": false,
+ "declaration": true,
- "allowJs": true,
// == 変更点 ==
"jsx": "preserve",
"lib": [
"es2018",
"dom"
],
/* Strict Type-Checking Options */
"strict": true,
/* Additional Checks */
/* Report errors on unused locals. */
"noUnusedLocals": true,
/* Report errors on unused parameters. */
"noUnusedParameters": true,
/* Report error when not all code paths in function return a value. */
"noImplicitReturns": true,
/* Report errors for fallthrough cases in switch statement. */
"noFallthroughCasesInSwitch": true
},
"include": [
"src/**/*"
],
"exclude": [
".git",
"node_modules"
]
}
この状態でnpm run buildしてlib/以下へ.d.tsと.jsが生成されていれば成功です。
5. テストコードを(test/)をTypeScriptへ変換する
最後にテストコードもTypeScriptへ変換していきます。
これもソースコードと同じように.jsを.tsにしていくだけです。
基本的な考え方はソースコードの場合と同じです。
次の記事でも書いていましたが、テストコードは普通はファイルごとに独立しているので、必要になったら変換していく形でも問題ありません。
ソースコードの型定義をちゃんとしたい場合は、テストコードもTypeScriptで書いておくと自然とまともな型になっていきます。
テストコードも.tsに変更できたらtest/tsconfig.jsonからもallowJsを外して完成です。
{
"extends": "../tsconfig.json",
"compilerOptions": {
"declaration": false,
- "allowJs": true,
"noEmit": true
},
"include": [
"../src/**/*",
"./**/*"
]
}
Notes: .tsにするとテストコードが通らなくなる
忘れがちですが、.jsという拡張子に依存している処理が存在しているケースもあります。
特に動的に.jsをfsで読み込んでいる場合などは、ファイルが存在しなくなるため失敗することもあります。
npm へ publishする
基本的にはBabelのときと同じようにlibをnpmへpublishします。
型定義ファイルも生成されるようになったので、typesフィールドを追加して型定義ファイルも配布するのを忘れないでください。
{
// typesにd.tsのパスを追加する
"main": "./lib/main.js",
"types": "./lib/main.d.ts"
}
これで使う側も、TypeScriptで型付きのライブラリとして利用できます。
Notes: Babelの不要なファイルを削除する。
TypeScriptへ移行できたら、Babelの依存は不要なので削除しても大丈夫です。
.babelrcdevDependenciesのbabel
また、lib/には古い変換結果が残っている場合があるので、一度lib/を消してしまうのが良いでしょう。
おわりに
この記事では、Babelで書いていたライブラリをTypeScriptへ変換する方法を紹介しました。
textlint-rule-helperはライブラリをES2015(ES6)で書いて公開する所から始めよう | Web ScratchでBabelでライブラリを書く例として紹介しています。 この記事では、そのライブラリをTypeScriptへ変換しました。
今のTypeScriptは、Babelからの変換はかなりスムーズに行えます。
実際にこの記事で書いた変換は、30分程度でできています。
記事では省略せずに書いていますが、次のスクリプトでステップ1-3まではほぼ自動化できます。 (スクリプトにはコピー元のファイルが入ってなかったり、そのままではビルドは通らないですが)
- [自動] TypeScriptをインストールする
- [自動] TypeScript(tsc)でJavaScriptをビルドできるようにする
- [自動] TypeScript(tsc)でJavaScriptのテストを通るようにする
- ソースコード(src/)をTypeScriptへ変換する
- テストコードを(test/)をTypeScriptへ変換する
#!/bin/bash
# variable
declare scriptDir=$(cd $(dirname ${BASH_SOURCE:-$0}); pwd)
declare currentDir=$(pwd)
declare currentDirName=$(basename "${currentDir}")
# dependecy script
npm install npe sort-package-json --global
function echo_message(){
echo "\033[31m=>\033[0m \033[036m$1\033[0m"
}
# Install
echo_message "npm install"
yarn add --dev --pure-lockfile \
typescript \
ts-node \
ts-node-test-register \
mocha \
@types/node \
@types/mocha \
cross-env
# Copy config
echo_message "Copy .tsconfig mocha.opts"
mkdir -p test
## !!!!ここはコピー元のファイルが必要なのでこのままでは動かない
cp ${scriptDir}/resources/tsconfig.json ./
cp ${scriptDir}/resources/test.tsconfig.json ./test/tsconfig.json
cp ${scriptDir}/resources/typescript.mocha.opts ./test/mocha.opts
# Edit package.json
## Add script
echo_message "Add npm run-script"
npe scripts.build "cross-env NODE_ENV=production tsc -p ."
npe scripts.watch "tsc -p . --watch"
npe scripts.prepublish "npm run --if-present build"
npe scripts.test "mocha \"test/**/*.{js,ts}\""
npe types "lib/${currentDirName}.d.ts"
sort-package-json
# git
git add .
BabelからTypeScriptへの移行は典型処理も多いので、慣れるとそこまで難しくはありません。 TypeScriptに変換することで型定義ファイルを配布できたり、型チェックが利用できるというメリットもあります。 また、外部に型定義ファイルを作成するよりも、ソースコード自体をTypeScriptに変換してしまったほうが型定義ファイルを作成するのが楽というケースも多いです。
一方、TypeScriptにはBabelのようなプラグインでのエコシステムはあまりないため、その辺の自由度は減ります。 しかし、ライブラリにおいては@babel/preset-env以外のBabelプラグインを導入するケースは、そこまで多くはないと思います。(babel-preset-power-assertなどテストコードへの補助的なものを入れることはあると思います)
ライブラリをTypeScriptにすることにメリットを見いだせる場合は、TypeScriptへ移行してみるのもよいのかもしれません。
リポジトリ
- textlint-rule-helper
- refactor(TypeScript): Convert to TypeScript by azu · Pull Request #11 · textlint/textlint-rule-helper
参考
FAQ
- Q. @babel/preset-typescriptではだめ?
- A. @babel/preset-typescriptはTypeScriptから型情報を取り除くプリセットで、型定義ファイル(d.ts)を生成したり、型チェックはできません。そのため、ライブラリをTypeScriptに移行する際に、
tscを使うことに比べて特にメリットがありません。(どちらにしてもtscの併用が必要になるため、あまり意味がありません) - @babel/preset-typescriptはアプリケーション向けだと思います。Babelのエコシステムという柔軟性を取り入れつつ、TypeScriptで書けるメリットを享受できます。
お知らせ欄
JavaScript Primerの書籍版がAmazonで購入できます。
JavaScriptに関する最新情報は週一でJSer.infoを更新しています。
GitHub Sponsorsでの支援を募集しています。