1. はじめに
TypeScriptを使ってNode.jsのプロジェクトを開発するとき、よくあるのは以下のようなフォルダ構成だと思います。
|- src/
| |- index.ts
| `- models/
| |- file1.ts
| `- file2.ts
|- dist/
| |- index.js
| `- models/
| |- file1.js
| `- file2.js
|- test/
| |- index.ts
| `- models/
| |- file1.ts
| `- file2.ts
|- README.md
|- .gitignore
|- package.json
|- yarn.lock
`- tsconfig.json
これも悪くないですが、例えば静的ファイルを開発するときはどうしましょうか?
src/
に入れますか? dist/
に入れますか?それともリポジトリの直下に置きますか?
静的ファイルだと開発する機会は限られるかもしれませんが、コンパイルしないファイルにまで拡げると、そういうケースは結構あると思います。
提案したいこと
src/ディレクトリを作るのを止めませんか?
TypeScriptとして一般的な上のような構造を、次のような構造に作り替えます。
|- README.md
|- .gitignore
|- package.json
|- yarn.lock
|- tsconfig.json
|
|- index.ts
|- index.js
|- index.test.ts
`- models/
|- file1.ts
|- file1.js
|- file1.test.ts
|- file2.ts
|- file2.js
`- file2.test.ts
ポイントとしては、TypeScriptのファイルもJavaScriptのファイルもテストコードも全て横並びに配置します。
単体テストが作るような世界で、ソースコードとテストコードを離すよりも、隣り合わせに配置してお互いをより意識的に関係性を持たせようという意図があります。
2. 基本的な構造
2-1. フォルダ構成
前述(提案したいこと)にあるとおり、『ソースコード』・『コンパイルされた実行コード』・『テストコード』の三つを隣り合わせに配置します。
なお、それぞれのファイルの拡張子は、次のように付けます。
コードの種類 | 拡張子 |
---|---|
ソースコード | *.ts |
コンパイルされた実行コード | *.js |
テストコード | *.test.ts |
上のようなファイルの拡張子に基づき、以下のように配置します。
|- package.json
|- yarn.lock
|- tsconfig.json
|- index.ts
|- index.js
|- index.test.ts
`- models/
|- file1.ts
|- file1.js
`- file1.test.ts
2-2. コンパイル設定
TSファイルのコンパイル時に気を付けないといけないのは、『ソースコード』・『テストコード』が同じフォルダ内にあり、そのファイルの在り処はバラバラです。
その状況でしっかりとコンパイルするための設定が必要になります。
そのために重要になってくるのが、 include
と exclude
です。
再帰的に全てのTSファイルが対象になるようにしつつ、 node_modules
やテストコードファイルが対象外になるように設定することが必要です。
その設定が以下になります。
{
"compilerOptions": {
"target": "ES2019",
"module": "commonjs",
"strict": true,
"esModuleInterop": true
},
"include": [
"**/*.ts"
],
"exclude": [
"node_modules",
"**/*.test.ts"
]
}
2-3. カバレッジの設定
テストコードの実行には、 nyc
と mocha
を使います。
また、TypeScriptのテストコードを実行するために ts-node
も使用します。
まず、nyc
の設定をします。
以下のように全てのTSファイルをカバレッジの対象とし、その上で型定義ファイル、テストコードファイル、さらにコンパイルしたファイルをカバレッジの対象から除外します。
"nyc": {
"extension": [
".ts"
],
"include": [
"**/*.ts"
],
"exclude": [
"**/*.d.ts",
"**/*.test.ts",
"**/*.js"
],
"require": [
"ts-node/register"
],
"reporter": [
"text",
"html"
]
}
次に、テストを実行するスクリプトを作成します。
以下のコードで全てのテストコードを実行することができます。
"scripts": {
"test": "yarn run nyc mocha **/*.test.ts"
}
3. 難しいポイント
3-1. テストコードで.tsのファイルがimportされない
原因
import
されるファイルの優先順位が .ts
のファイルに比べて .js
のファイルが高いです。
そのため、コンパイルして .js
ファイルが生成されている状態だと、 .ts
が読み込まれず、 .js
ファイルが読み込まれます。
もしコンパイルしたファイルが存在していない状態だと、望んだ通りに .ts
のファイルが読み込まれます。
解決法
コンパイルしたファイルを削除した上で、テストコードを実行します。
そのTypeScriptによってコンパイルしたファイルを削除する機能は、TypeScriptのコンパイラである tsc
が持っています。
yarn run tsc --build --clean
# Syntax: tsc --build [options] [file...]
# --clean Delete the outputs of all projects
なので、これを使って yarn
で実行するスクリプトを作成します。
作成するものとしては、『コンパイルするもの』・『コンパイルしたものを削除するもの』・『処理を実行するもの』・『テストコードを実行するもの』とその必要な組み合わせです。
"scripts": {
"build": "yarn run tsc --build",
"clean": "yarn run tsc --build --clean",
"main": "node index",
"mocha": "yarn run nyc mocha **/*.test.ts",
"start": "yarn run build && yarn run main",
"test": "yarn run clean && yarn run mocha"
}
これで、テストを実行する前にコンパイルしたファイルを削除することで、 .ts
のファイルが読み込まれて、カバレッジの算出で .ts
のファイルが使用されます。
4. さいごに
開発にこれと決まった方法はありません。
方法論は日々変わっていきますし、開発を共に行うチームによって選ぶべきものは変わるでしょう。
だからこそ、取ることのできる選択肢を増やすことは大事だと考えています。
この記事で紹介した方法を選択肢の一つに入れてもらえると幸いです。
最後に、具体例を以下に示します。
4-1. Expressアプリケーション
Express を使ったアプリケーションの開発で使うことができます。
通常 TypeScript
× Express
の場合、 Node Starter を使うことが多いでしょう。
この記事の構造を参考にすることで、 Expressジェネレーター を使って開発を始めることができます。