[TypeScript環境構築] VSCodeとWSLを活用した効率的な開発環境の構築

### **はじめに**
この記事では、TypeScript学習のための実践的な開発環境の構築手順を紹介します。
学習環境としての構築ですが、実際の開発にも耐え得る環境を構築しているのでぜひ活用してみてください。

本記事ではWSLとVSCodeのセットアップは解説していません。
**WSLとVSCodeセットアップ方法は[こちら](https://qiita.com/kappayarou/items/b2de0693a7b906250af4)** をご覧ください。

---

### **この記事でわかること**
・WSLとVSCodeを組み合わせた環境でのTypeScriptプロジェクトセットアップ手順
・TypeScriptからJavaScriptへのトランスパイルの流れとその手法
・TypeScriptとJavaScriptの実行方法
・ESLintとPrettierの導入と統合

---
### **目次**
1. npmのインストールとプロジェクト初期化
2. TypeScriptのセットアップ
3. ts-nodeの導入から学ぶトランスパイルと実行の仕組み
4. 継続的トランスパイル
5. ESLintとPrettierの導入と統合
6. スクリプト設定と便利コマンドの活用

---

### **1. nvmのインストールとプロジェクト初期化**
TypeScriptを導入する前段階として、`nvm`のインストールと`npm`の初期化を実行します。
`nvm`は`Node Version Manager`の略で、`node.js`のバージョン管理を行ないます。
`npm`は`node package manager`の略で、nodeプロジェクトの依存関係を管理してくれるパッケージマネージャーです。
TypeScriptは JavaScript のスーパーセットであるため、実行には JavaScript のランタイムである`Node.js`（あるいはブラウザ等）が必要になります。
また、`nvm`はあくまで`node.js`のバージョン管理であるため、依存関係は専用のパッケージマネージャーが必要です。
今回は`npm`で依存関係を管理します。
`node.js`のパッケージマネージャーは`npm`以外にもありますが、本記事では取り扱いません。

1. **nvmのインストール**
   ```bash
   sudo apt install nvm
   node -v  # node.jsのバージョンが表示される。筆者の環境は v22.12.0
   npm -v  # npmのバージョンが表示される。筆者の環境は 11.0.0
   ```
   - `nvm`をインストールすると、`npm`も一緒にインストールされます。

2. **プロジェクト初期化**
   ```bash
   npm init -y  # package.jsonが自動生成される
   ```
   - 作成された `package.json` の `"type"` を `"module"` に変更します。`type`はモジュールのインポート形式を指定しており、JavaScript modules（`ESM`）と`CommonJS`があります。
   - `CommonJS`は`node.js`が開発された初期の頃の古い規格であり、現在は`ESM`が主流となっています。`CommonJS`を指定する場合、トランスパイル後の`JavaScript`のバージョンを`ES5`（2025/1/18現在の最新は`ES2022`）という古いバージョンに指定する必要があり、実質的にESM一択と言えます。

---



### **2. TypeScriptのセットアップ**
TypeScriptでの開発を円滑にできるようにするための様々な準備をします。
このセクションでは、必要な準備を進めながら、なぜそれが必要なのかも理解できるように、解説に加えて適宜”想定通りではない結果”となる操作も実行します。
想定通りではない結果やエラーを通して、設定や操作の必要性を理解しつつ環境を構築していきましょう。

1. **TypeScriptのインストール**
   ```bash
   npm install typescript --save-dev  # TypeScriptが開発環境の依存関係としてインストールされる
   ```
   - `--save-dev`オプションを指定することで、TypeScriptを開発環境専用の依存関係としてインストールします。TypeScriptは開発中にのみ使用され、本番環境ではトランスパイル済みのJavaScriptが実行されるため、この設定が推奨されます。


2. **ディレクトリ作成**
   ```bash
   mkdir src dist
   ```
   - ソースファイル格納先の`src/`ディレクトリと、トランスパイルファイル出力先の`dist/`ディレクトリを作成。


3. **index.tsの作成**
   ```bash
   echo "console.log(\"complete npx tsc\");" > src/index.ts  # src配下にindex.tsを作成
   # トランスパイルして実行するとコンソールに complete npx tsc と表示されるスクリプト
   ```
   - TypeScriptファイルを作成し、トランスパイルを試す準備を整えます。


4. **index.tsのトランスパイル**
   ```bash
   npx tsc  # トランスパイルを実行するコマンド。エラーが発生する
   ```
   - トランスパイル対象もオプションも指定していないため、tscが何をすればいいのかわからないためエラーが発生します。
   - エラーを回避するには、対象を引数で渡すか、後述のtsconfig.jsonの設定が必要になります。

   ```bash
   npx tsc src/index.ts  # エラーなくindex.tsがトランスパイルされるが、src/配下に出力される
   ```
   - トランスパイル対象を引数として渡すことで正常にトランスパイルされます。
   - トランスパイル出力先として`dist/`を作成していましたが、`src`に出力されています。これは、出力先が指定されていないためです。
   - 対象と出力先を毎回指定すれば正しくトランスパイルされますが、毎回は面倒です。tsconfig.jsonを作成することでこの問題をまとめて解決できます。


5. **index.jsの実行**
   tsconfig.jsonの作成の前に、せっかくトランスパイルしたので、一度`index.js`を実行してみましょう。
   ```bash
   node src/index.js  # complete npx tsc がコンソールに表示される
   ```
   - `node <ファイルパス>.js` でJavaScriptファイルを実行できます。


6. **tsconfig.jsonの作成と設定**
   ```bash
   npx tsc --init  # TypeScriptプロジェクトの初期化。tsconfig.jsonが自動生成される
   ```
   - 自動生成された`tsconfig.json`には、基本的な設定と、コメントアウトされたその他設定が既にに記述されています。
   - 好みやチームの事情に応じてカスタマイズすればよいですが、最初はよくわからないと思うので筆者の設定を公開しておきます。必要最低限の設定のみとなっているため、必要に応じて追加や修正をするとよいでしょう。

   **設定例**：
     ```json
     {
       "compilerOptions": {
         "target": "ESNext",
         "module": "NodeNext",
         "rootDir": "./src",
         "outDir": "./dist"
       },
       "include": ["src/**/*"]
     }
     ```
   - `compilerOptions`：トランスパイルにかかわる設定を記述するセクション
   - `target`：トランスパイル結果を`ESM`と`CommonJS`のどちらに準拠するかとそのバージョンを記述する。つまり出力された`.js`の`ESM` or `CommonJS`とそのバージョン。`ESNext`は最新の`ESM`を指定。つまりESM latest。
   - `module`：トランスパイルにどのモジュールシステムを使用するかを指定。`Node.js`の`ESM`解決方式に合わせた `NodeNext`を指定。これも意味的にはESM latest。
   - `rootDir`：トランスパイル対象ファイルのルートディレクトリを指定する。なくてもトランスパイルは可能だが、ルートディレクトリを認識できないので、`dist/src/<ファイル名>.js`になったりする。
   - `outDir`：トランスパイル出力先を指定する。指定しないとトランスパイル元と同じ階層に出力される。
   - `include`：トランスパイル対象を指定する。指定しないと`tsconfig.json`と同階層以下の`.ts`,`.js`,`.mjs`等のJavaScriptやTypeScript系のあらゆるファイルを対象にしようとする。


7. **tsconfig.json作成後、もう一度トランスパイルと実行をしてみる**
   ```bash
   npx tsc  # dist/index.js が作成される
   node dist/index.js  # complete npx tsc がコンソールに表示される
   ```
   - トランスパイル対象もオプションも指定せずにトランスパイルが成功します。
   - トランスパイルファイルも`dist/`配下に生成されています。

---



### **3. ts-nodeの導入から学ぶトランスパイルと実行の仕組み**
このセクションでは、`tsc`を実行せずとも直接`.ts`を実行できる`ts-node`を導入し、いくつかの設定変更やエラーを通してトランスパイルと実行についての理解を深めていきます。
**環境構築手順というよりは学習目的が強いので、とにかく構築を進めたい人は飛ばしていただいても大丈夫です。**

1. **トランスパイル操作をせず直接実行できるように ts-node の導入**
   ```bash
   npm install ts-node --save-dev
   npx ts-node src/index.ts  # TypeError: Unknown file extension ".ts" が発生してエラーとなる
   ```
   - ` npx ts-node <ファイル名>.ts`で直接tsファイルを実行できます。ただし、今回はエラーが発生しています。
   - このエラーは`.ts`ファイルを`node.js`が認識できないという意味のエラーです。
   - `node src/index.ts`でもまったく同じエラーが発生します。
   - `package.jon`の`type`を`module`から`commonjs`に変更するとエラーが解消されます。次項で試してみましょう。

2. **package.jsonのtypeをcommonjsに変更し、ts-nodeを試してみる**

   ```json
      "keywords": [],
      "author": "",
      "license": "ISC",
      "type": "commonjs",
      "description": "",
      "devDependencies": {
        "globals": "^15.14.0",
        "ts-node": "^10.9.2",
        "typescript": "^5.7.3"
      }
   ```
   ```bash
   npx ts-node src/index.ts  # エラーなく実行できる
   ```
   - `commonjs`で実行できて、`module`（`ESM`）で実行できない理由は、`ts-node`の実行方式とフックが`ESM`に対応していないからです。
   - 次項では`ts-node`のフックおよび実行方式と、`tsc`によるトランスパイルと実行の違いについて解説します。


3. **ts-nodeのフックとnodeによる実行の仕組み**
   nodeによる実行とts-nodeによるフックについて解説していきます。
   まずは、commonjsにおける実行の仕組みです。

   **commonjsにおける実行の仕組み**
   ```
   node index.js ⇒ require('index.js') ⇒ index.js を実行
   ts-node index.ts ⇒ require('index.ts') ⇒ (フックで ts-node がトランスパイル) ⇒ require('index.js' 相当のコード) ⇒ index.js を実行
   ```
   - フックとは、ある処理が行われる前または後に処理を差し込んで実行する仕組みのことです。
   - `ts-node`は`commonjs`のモジュール読み込み処理である`require()`の前にトランスパイルするフックを持っています。
   - `node.js`が実行のために`require()`を実行する際に、`ts-node`はそのファイルをトランスパイルします。これにより、`.ts`ファイルを指定しても最終的に`node.js`が実行するファイルは`.js`となります。（正確にはフックの後の処理はオンザフライと呼ばれる方式で、トランスパイル後のソースを直接`node.js`に渡しています。）
   - トランスパイルしたソースを`node.js`に直接渡しているため、`dist/`ディレクトリにファイルが出力されません。気になる方は`dist/index.js`を削除して試してみてください。

   次に、ESMにおける実行の仕組みを説明します。

   **ESMにおける実行の仕組み**
   ```
   node index.js ⇒ import index.js ⇒ index.js を実行
   ts-node index.ts ⇒ import index.ts ⇒ (フックが適用されないためエラー; Node.js が .ts を認識しない)
   ```
   - `ts-node`のフックは`require()`に対するフックであるため、`import`には反応しません。
   - 結果的に、`node.js`には`.ts`がそのまま渡されるため、認識できずにエラーとなります。


3. **ESMにおける.tsファイルの直接実行**
   `ESM`において、`ts-node`はそのままでは`.ts`ファイルを直接実行できません。しかし、ある方法で`ESM`でも`ts-node`による直接実行が可能になります。
   
   ```bash
   node --loader ts-node/esm src/index.ts  # 直接実行が成功する。ただし、ExperimentalWarningが表示される
   ```
   - この方法なら`.ts`の直接実行が可能です。ただし、警告文が表示されます。これは、このコマンドが実験段階のコマンドであり、正式リリースされたものではないからです。
   - `--loader`は`node.js`のモジュール読み込みに使うローダーを指定します。
   - `ts-node/esm`は`ESM`に対応した`ts-node`のフックをローダーとして提供します。
   - `node.js`のローダーとして、`ESM`に対応した`ts-node`のフックを指定することにより、`.ts`の直接実行を実現します。

このように、`node.js`のローダー指定と`ts-node`のフックを組み合わせることで`.ts`ファイルを直接実行できます。
しかし、この方式はまだ正式リリースではないため、予期しない動作や将来的に削除されるリスクがあります。
次節では、リスクがなく安定でありながら利便性の高いトランスパイルと実行の方式として、`tsc --watch`による継続的トランスパイルの方法を解説します。
（次節に進む前に`package.json`の`type`を`module`に戻しておきましょう）
   
---



### **4. 継続的トランスパイル**
`tsc`によるトランスパイルは確実で安定していますが、ファイルを変更するたびに実行するのは少々手間です。
前節では`ts-node`による`.ts`の直接実行方法を紹介しましたが、そのままでは`CommonJS`でしか実行できませんでした。`ESM`でも実行する方法も紹介しましたが、実験的機能であり正式採用するにはリスクがあります。
ここでは、トランスパイルを自動かつ継続的に実行する方法を紹介します。

1. **ターミナルの分割**
   ターミナルの右端にある”+”マークをクリックします。
   ターミナルが複製され、右端にターミナルを選択するウィンドウが追加されます。
    - 右端のターミナル名を 右クリック ⇒ 名前の変更 で名前を変更できます。
    - ゴミ箱マークをクリックすれば対象のターミナルを終了できます。

2. **継続的トランスパイルの起動**
   ```bash
   npx tsc --watch  # 継続的トランスパイルが常駐する
   ```
   - 継続的トランスパイルを実行する`--watch`オプションで`tsc`を常駐します。
   - 常駐するため、`--watch`を実行中のターミナルではほかの操作ができません。複製したターミナルで実行することで元のターミナルで操作を継続できます。（コマンド末尾に`&`をつけてバックグラウンド実行したり等、ほかにもいろいろ方法はあります。あくまで一例です）
   - `ctrl + c`で終了できます。


2. **.tsファイルを修正し、自動コンパイルされていることを確認**
   ```bash
   echo "console.log(\"watch test\");" > src/watch_test.ts  # src配下に--watch確認用の.tsファイルを作成
   node dist/watch_test.js  # watch test
   ```
   - すぐに自動トランスパイルされ、`dist/watch_test.js`が生成される。
   - `.ts`作成後、即座に実行すると間に合わずに`Error: Cannot find module`となる場合がある。エラーを認識した瞬間にはもうトランスパイルされているはずなので、もう一度実行しよう。

---



### **5. ESLintとPrettierの導入と統合**
前節までの準備で、TypeScriptの実行環境は整いました。
このままでも開発は可能ですが、せっかくなら安全で読みやすいコードを書けるようにさらに環境を整えていきましょう。
このセクションでは、linterとしてTypeScriptの文法や型をチェックし自動修正してくれるESLintと、フォーマットチェックと自動修正をしてくれるPrettierの導入を行ないます。

1. **ESLintのインストールと初期化**
   ```bash
   npm install eslint --save-dev  # ESLintのインストール
   npx eslint --init  # ESLintの初期化。いくつかの質問が表示され、回答に合わせて初期化内容が変化する
   ```
   - 初期化時の質問と回答例：
     - 質問：How would you like to use ESLint?
       - 回答：To check syntax and find problems
         - 補足：eslintを構文チェック以外の種々の問題確認にも適用

     - 質問：What type of modules does your project use?
       - 回答：JavaScript modules (import/export)
         - 補足：モジュールのインポート形式の選択。moduleはESMを指す

     - 質問：Which framework does your project use?
       - 回答：None of these
         - 補足：React、vue.js、プレーンなTypeScriptのいずれかを選択できる。
         - 補足：今回は純粋なTypeScriptの環境を構築するのでNone of theseを選択

     - 質問：Does your project use TypeScript?
       - 回答：Yes
         - 補足：TypeScriptの環境なのでyes

     - 質問：Where does your code run?
       - 回答：Nodeのみ
         - 補足：今回は純粋なTypeScript環境で、ブラウザ実行する予定はないためNodeのみとする
         - 補足：ブラウザのチェックは外しておく（スペースキーでオンオフ切り替え可能）

     - 質問：The config that you've selected requires the following dependencies:
              eslint, globals, @eslint/js, typescript-eslint
              ? Would you like to install them now? ‣ No / Yes
       - 回答：Yes
         - 補足：依存関係としてeslint, globals, @eslint/js, typescript-eslintをインストールが必要だが、インストールしてよいか聞かれている。インストールが必要なのでyes。

     - 質問：Which package manager do you want to use?
       - 回答：npm
         - 補足：使用しているパッケージマネージャーを選択。今回はnpmを使用しているので、npm

   質問にすべて答えるとeslintの設定ファイルであるeslint.config.mjsが自動生成されます。

2. **Prettierの導入**
   ```bash
   npm install prettier --save-dev  # Prettierのインストール
   touch .prettierrc  # 設定ファイルの作成。Prettierの設定ファイルは自動生成されない
   ```
   - Prettierの設定ファイルは自動生成されないため、手動で作成する必要があります。

   `.prettierrc`を下記のように修正
   
     ```json
     {
       "semi": true,
       "singleQuote": true,
       "tabWidth": 2,
       "trailingComma": "all"
     }
     ```
     - `"semi": true`：文末にセミコロンを強制
     - `"singleQuote": true`：ダブルクオートとシングルクォートのうち、シングルクォートを強制
     - `"tabWidth": 2`：インデントを2byteに強制
     - `"trailingComma": "all"`：可能なすべての場所にカンマを付けることを強制


3. **統合用パッケージの導入**
   ```bash
   npm install eslint-config-prettier eslint-plugin-prettier --save-dev
   ```
   - `eslint-config-prettier`は`ESLint`と`Prettier`のルール競合を回避する設定を提供します。`ESLint`はフォーマッターの機能もあるため、そのままだと`Prettier`と競合が発生します。
   - `eslint-plugin-prettier`は`ESLint`に`Prettier`のルールを適用するためのプラグインです。`ESLint`のフォーマッター機能を`Prettier`に置き換えます。


4. **ESLintとPrettierの統合設定**
   `ESLint`と`Prettier`を統合するために、`eslint.config.mjs`の設定を修正します。
   ここでは、自動生成された`eslint.config.mjs`の内容と、修正後の内容を載せるのみに留めます。
   各設定の意味と意図についてはすべてコメントで補足してありますので、皆さんの理解の助けになれば幸いです。
   正直、ここはかなりの鬼門で相当な試行錯誤をしました。公式リファレンスを確認しながら試行錯誤を重ねた結果となるので、最適解であると断言はできないです。より効率的な設定や高機能な設定をご存じの方はコメントで教えていただけると大変助かります。

   - 自動生成された`eslint.config.mjs`
      ```TypeScript
      import globals from "globals";
      import pluginJs from "@eslint/js";
      import tseslint from "typescript-eslint";


      /** @type {import('eslint').Linter.Config[]} */
      export default [
        {files: ["**/*.{js,mjs,cjs,ts}"]},
        {languageOptions: { globals: globals.node }},
        pluginJs.configs.recommended,
        ...tseslint.configs.recommended,
      ];
      ```

   - 修正後の`eslint.config.mjs`
      ```TypeScript
      import globals from "globals"; // node用のglobal変数をインポート
      import eslint from "@eslint/js" // js用のeslint。ベースとなる。
      import tseslint from "typescript-eslint"; // TypeScript用のeslint。拡張設定。
      import tsParser from "@typescript-eslint/parser"; // TypeScriptのAST解析用のparser。静的解析をサポート。
      import prettierConfig from "eslint-config-prettier";  // ESLint のスタイルルールとの競合を無効化するパッケージ。
      import prettier from "eslint-plugin-prettier"; // Prettier を ESLint のルールとして利用するためのプラグイン。


      /** @type {import('eslint').Linter.Config[]} */
      export default [
        {
          ignores: [
            "**/dist/**/*", // distディレクトリをlint対象から除外
            "**/eslint.config.*", // この設定ファイル自身をlint対象から除外
          ],
        },
        {
          files: ["src/**/*.ts"], // この設定ファイルを適用する対象をsrc配下に限定。この設定をしてもlint対象から除外されるわけではなく、ルール適用外になるだけ。除外はignoresで。
        },
        ...tseslint.config( // flat configでは設定を配列で列挙する。tseslint.config(...)は配列を返すが、export default [...]で配列形式で記述しているため、入れ子になる。よってスプレッド構文で展開。
          eslint.configs.recommended, // tseslintの設定として、eslintの推奨設定を適用。
          tseslint.configs.recommended, // tseslintの設定として、tseslintの推奨設定で設定を拡張
          tseslint.configs.strict, // eslintの型認識を厳格化。
        ),
        {
          plugins: {
            prettier, // Prettier を ESLint のルールとして利用するためのプラグイン。
          },
        },
        {
          languageOptions: {
            globals: globals.node, // nodeのデフォルト。nodeのグローバル変数をインポート。
            ecmaVersion: 15, // ECMA Scriptのバージョン。tsconfig.jsonと合わせておく。
            sourceType: "module", // ESMかCommonJSか。ESMの場合はmodule。
            parser: tsParser, // AST解析で使用するparserを設定する。TypeScriptなのでtsParserを指定。
          },
          rules: {
            "prettier/prettier": "error", // prettierルールの違反をエラーとして扱う。
            ...prettierConfig.rules, // prettierと競合するeslintルールの無効化をする設定の展開。{rules: {...}}のJSON形式であるため、スプレッド構文でrulesまで展開する。
          },
        },
      ];

      ```


4. **ESLintとPrettierのリアルタイム解析の導入**
   - VSCodeの拡張機能で、`ESLint`を検索し、`ESLint`をインストールします。
   - VSCodeの拡張機能で、`Prettier`を検索し、`Prettier – Code formatter`インストールします。
   
   ESlintとPrettierは、本来はコマンドラインで実行することで解析や修正を行なうパッケージです。
   しかし、型やスタイルが正しいかをいちいちコマンドで解析するのは手間です。
   拡張機能を導入することで、解析をリアルタイムに反映することができます。
   
---



### **6. スクリプト設定と便利コマンドの活用**
`package.json`には`scripts`という、スクリプトを記述するセクションがあります。
記述したスクリプトは`npm run <スクリプト名>` で実行することができます。
スクリプトはLinuxのコマンドエイリアスに近いイメージです。
ここでは、筆者が設定したスクリプトを紹介します。

1. **package.jsonの設定**
   ```json
     "scripts": {
       "build": "tsc",
       "start": "tsc && node dist/index.js",
       "run": "node $npm_config_file",
       "tsnode": "ts-node $npm_config_file",
       "tsnode:esm": "node --loader ts-node/esm $npm_config_file",
       "watch": "tsc --watch",
       "lint": "eslint .",
       "lint:fix": "eslint . --fix",
       "format:check": "prettier src/ --check",
       "format": "prettier src/ --write"
     }
   ```
   - `build`：トランスパイルを実行します。npx tscのほうが短いですが、`npm run`の形式で統一することができます
   - `start`：トランスパイルと`index.js`を実行します。基本的なアプリケーション設計ではメインとなるファイルから必要なモジュールが呼び出されるため、起点となるファイルを指定します。
   - `run`：任意の`.js`を`node`で実行します。`$npm_config_file"`は引数で渡したファイルを読み込みます。
     - 実行例：`npm run run index.js` ⇒ `complete npx tsc`が出力
   - `tsnode`：`ts-node`で指定した`.ts`ファイルを実行します。ただし、`ts-node`導入済で`type`が`commonjs`の場合のみ成功します。詳しくは`3章 ts-nodeの導入から学ぶトランスパイルと実行の仕組み` を参照ください
   - `tsnode:esm`：`ESM`環境で`.ts`ファイルを実行します。ただし、実験的機能であるため警告が表示されます。詳しくは`3章 ts-nodeの導入から学ぶトランスパイルと実行の仕組み` を参照ください
   - `watch`：`tsc --watch`を常駐します。
   - `lint`：`ESLint`による静的解析を実行します。ただし、拡張機能でリアルタイム解析されているため、基本的には実行することはありません。
   - `lint:fix`：ESLintによる静的解析を実行し、さらに自動修正を実行します。
   - `format:check`：`Prettier`によるフォーマットチェックを実行します。こちらもリアルタイムで解析されているため、基本的には実行することはありません。
   - `format`：`Prettier`によるフォーマット自動修正を実行します。


2. **実行例**
   - ESLintチェック:
     ```bash
     npm run lint  # src/watch_test.tsにエラーが検知される。
     # watch_test.tsはダブルクォートを使用しているため、シングルクォートのみの設定に違反
     ```
   - ESlintの自動修正:
     ```bash
     npm run lint:fix  # src/watch_test.tsが自動修正される
     # watch_test.tsがシングルクォートに自動修正されている
     ```

---

### **まとめ**
この記事では、TypeScriptの学習用の開発環境構築方法を解説しました。この環境を基にTypeScriptを活用した開発を楽しんでください！

---