TypeScriptの開発環境をDev Containersで作る手順まとめ

# はじめに

振り返るとだいぶ手順が多かったのでまとめてみます。

:::note warn
この記事は、書き始めた時期と書き終わった時期に数ヶ月ほどの差があります。
そのため一部チグハグなところがあると思いますが、目を瞑っていただけるとありがたいです。

また、その数ヶ月の間にコンテナの起動方法をDocker ComposeからDev Containersに変えたので、そのあたりが特にチグハグだと思います。
:::


## 作る環境

* TypeScript
* Jest
* VSCode（ローカルPCにインストール済み）
* Git

みたいなのを、Dev Containersという拡張機能を使ってDockerで作ります。


## ローカルの環境

* OS：MacOS Sonoma 14.1 (Apple M1)
* ターミナル：ITerm2をVSCodeから使ってる
* エディタ：VSCode（拡張機能あり）
    * Git Graph
    * Docker
    * Dev Containers
    * Jest
    * など...
* Docker Desktopインストール済み


## 愚痴

私はもともとJavaScriptの学習をしていたのですが、色々やっていくうちにTypeScriptいいなと思い始めました。

ですが、TSはJSと違ってコンパイラ（トランスパイラ）が必要です。
そしてローカルPCにコンパイラを入れるのが嫌だったため、TSをやるときはいつもDockerを立てています。

そして毎回1からDocker立ててTypeScript入れてJest入れて設定ファイル編集して...とかやっているのですが、これはあまりに非効率だと昨日気づきました。
ただ自動化する方法がわかんないので辛いです。

まあそれでも今後少しでも楽になるように、今日のところは上に書いた開発環境を作る手法をメモっておこうと思います。


# 結論

これは先日~~怒りの感情に任せて書いた~~メモです。
これだけだと分かりづらいので、下に詳しい解説を書いていこうと思います。

1. `devcontainer.json`をコピペで作る
    * TypeScriptとJestを入れる
    * `node_modules`をボリュームにする
    * 拡張機能のセッティングする
1. `tsconfig.json`のセッティング
    * `tsc --init`で`tsconfig.json`を生成する
    * `rootdir`を`./src`にする
    * `outdir`を`./build`にする
    * `target`を`es2022`にする
    * `resolveJsonModule`を`true`にする
    * 一応動くか試してみる
2. Jestのセッティング(`jest.config.js`)
     * TypeScriptでテストを書くには追加でパッケージを入れる必要がある
        * `@types/jest`
        * `ts-jest`
    * まずは`jest --init`で`jest.config.js`を生成する
        * configファイルでTypeScriptを使うか？に`y`と答えるとやり直し
        (`ts-node`が必要って言われる)
    * デフォルトだと`import`使えない問題を解消する
    (`ts-jest`を入れた上で`preset`に`ts-jest`を指定する)
    * テストファイル作って一応動かしてみる
        * だいたいここでなんかエラー出るので調べる
1. `.gitignore`を作って以下を書く
    * `node_modules` インストールしたパッケージがある場所
    * `build` コンパイルしたjsファイル置き場
    * 時と場合によっては`.gitignore`
    * やってるとだんだん増えてゆく


# 1. Dockerコンテナを立てる

まずはDockerコンテナを立てていきます。
忙しい方は下の[まとめ](#1のまとめ)を読んでみてください。


## devcontainer.jsonの作成

昔は`dockerfile`と`compose.yaml`を作っていたのですが、最近は`.devcontainer/devcontainer.json`でやるようにしています。

これはVSCodeのDev Containerという拡張機能用のファイルで、Dockerコンテナの構築をやってくれる便利なものです。

https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers

以下のファイルを新しく作ります。

```json:.devcontainer/devcontainer.json
{
  "name": "Dev Steps",
  "image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye",
  "workspaceFolder": "/project",
  "workspaceMount": "source=${localWorkspaceFolder},target=/project,type=bind",
  "mounts": [
    "target=${containerWorkspaceFolder}/node_modules,source=dev-steps-volume"
  ],
  "features": {
    "ghcr.io/devcontainers-contrib/features/jest": {
      "version": "latest"
    }
  },
  "customizations": {
    "vscode": {
      "extensions": [
        "Orta.vscode-jest",
        "shd101wyy.markdown-preview-enhanced"
      ]
    }
  },
  "onCreateCommand": "sudo chown node node_modules && yarn install"
}
```

ここでは、以下のことをやっています。

* ベースイメージにTypeScript + Node.jsを指定
* `node_modules`をボリュームにする
* Jestのインストール
    * テストフレームワーク
    * `features`のところに書ける
* ワークスペースに`/project`を指定
* 使いたい拡張機能のインストール
    * ここではJestとMarkdown Preview Enhanced
* `yarn install`を実行
    * `node_modules`の権限を`node`に変更

こんな感じです。
あとはここに適宜必要なものを追加していきます。

ちなみに`/project`というパスはプロジェクトルートです。
時と場合によって変わったりもします。
また、コンテナの`/project`に現在開いているワークスペースのフォルダを紐づけるため、`workspaceMount`を設定しています。

:::note info
`node_modules`をボリュームにする理由

`node_modules`はnpmやyarnでインストールしたパッケージ置き場なので、基本的に**ファイルの量が膨大**になります。
しかも一度`package.json`に追記してしまえば、**コマンドひとつ叩くだけでインストールができます。**
そんなものをローカルPCに置いておく必要があるのか...?
私が出した答えは**NO**でした。

`/project/node_modules`をボリュームにすると、このディレクトリの中身はコンテナとローカルで共有されなくなります。
なので、コンテナで`yarn install`すると、**コンテナにはパッケージがインストールされますが、ローカルPCにはインストールされません。**
:::


## コンテナを立てる

必要なファイルができたので、Dockerコンテナを立てていきます。
といってもコマンドを打つわけではなく、VSCode右下に出る通知からやります。

まずはウィンドウをリロードします。
これはコマンドパレットからやるといいと思います。

次に、右下に出てきた通知から「コンテナーで再度開く」をクリックします。

![container.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588083/5e7d7647-aed9-d27d-b038-99845b8f3ad0.png)

そしたらしばらく待ちます。
コンテナを起動しているため、結構時間がかかることもあります。

少し待ったら、`node_modules`だけがあるVSCodeが立ち上がると思います。
これで準備は完了です。


### 正常にできているか確認する

さて、ここまでで出来ていて欲しいのは以下です。

* Node.jsのインストール
* TypeScriptのインストール
* Jestのインストール
* `node_modules`のボリューム化
* ワークスペースが`/project`になっている

というわけで、まとめて確認していきます。

まずはVSCodeからターミナルを開きます。
すると、おそらくプロンプトが以下のようになっていると思います。

![prompt.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588083/6a228353-3219-0e20-134e-bb570f757d8c.png)

青い部分が`/project`となっていたら、ワークスペースが`/project`に設定されています。

では、ここからコマンドを打って各ツールがあるか確認します。

```shell:ターミナル
node --version
v20.11.1

tsc --version
Version 5.3.3

jest --version
29.7.0
```

バージョンの差異はあれど、だいたいこんな感じに出力と思います。
エラーなく各コマンドが実行できれば成功です。

また、VSCodeのエクスプローラーに`node_modules`があれば、おそらく`node_modules`のボリューム化は出来ています。
あとは拡張機能が有効されているか確認してみてください。


## Gitを設定する

せっかくならGitの設定もここでやってしまいましょう。


### Gitを初期化する

まずは初期化です。
律儀に`git init`コマンドを打ってもいいですが、せっかくVSCodeを使っているのでVSCodeからやります。

サイドバーのソース管理アイコンを押して、「Initalize Repository」を押します。
（画像は[私の前の記事](https://qiita.com/nanasi-1/items/ed6fd328cd547316e4f6)の使い回しですが、実際は英語です）

![init.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588083/5a1348eb-c981-54bc-2d53-d9f6276a3879.png)

ちなみに、VSCodeからGitを操作する方法を記事にしてるので、良かったら見てみてください。

https://qiita.com/nanasi-1/items/ed6fd328cd547316e4f6


### .gitignoreを作る

Gitで管理しないフォルダ/ファイルを設定していきます。

私は基本的に以下を使っています。

```plaintext:.gitignore
.DS_Store
build
node_modules
```

* `.DS_Store` ... Macが内部で使っているファイル、Git管理する必要はない
* `build` ... TypeScriptをコンパイルしたJSファイルが入る場所
* `node_modules` ... パッケージ置き場


### コミットする

ついでにコミットしておいたほうがいいと思います。
設定ミスってぐちゃぐちゃになる前にやっておくと、後戻りできて楽です。


## 1のまとめ

1. 拡張機能群を入れる
    * [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)

1. `.devcontainer/devcontainer.json`を作る

    <details><summary>deccontainer.jsonのテンプレ</summary>

    ```json:.devcontainer/devcontainer.json
    {
      "name": "Dev Steps",
      "image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye",
      "mounts": [
        {
          "type": "volume",
          "source": "dev-steps-volume",
          "target": "${containerWorkspaceFolder}/node_modules"
        }
      ],
      "features": {
        "ghcr.io/devcontainers-contrib/features/jest": {
          "version": "latest"
        }
      },
      "workspaceFolder": "/project"
    }
    ```
    </details>

1. コンテナを立てる
    1. VSCodeをリロードする
    1. 通知から「コンテナーで再度開く」を選択する

1. うまく出来ているか確認する
    * ターミナルのプロンプトに`/project`があることを確認する
    * 以下コマンドを打ってみる
        * `node --version`
        * `tsc --version`
        * `jest --version`
    * エクスプローラーに`node_modules`があることを確認する
    * 拡張機能が有効化されていることを確認する
1. Gitを設定する
    * VSCodeのサイドバーから初期化する
    * `.gitignore`を作る
        * `node_modules`
        * `build`
        * `.DS_Store`
    * コミットする


# 2. tsconfig.jsonを書き換える

さて、[1で作った`devcontainer.json`](#devcontainerjsonの作成)には、`typescript`と`jest`をインストールする設定が書いてありました。
そのため、もうすでにこのコンテナにはTypeScript（とJest)が入っています。

ということで、さっそく`tsconfig.json`を書き換えていきます。


## ファイルを生成する

まずはターミナルに以下のコマンドを入力して、`tsconfig.json`を生成します。
```sh:tsconfig.jsonを生成する
tsc --init
```

すると何やら長いファイルが生成されます。

```jsonc:tsconfig.json
{
  "compilerOptions": {
    /* Visit https://aka.ms/tsconfig to read more about this file */

    /* Projects */
    // "incremental": true,                              /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
    // "composite": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */
    
    // 以下略
}
```


## 設定を書き換える

そしたら、ここの設定をお好みで書き換えていきます。
私は以下のように設定することが多いです。

```jsonc:tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022", // なんとなく
    "rootDir": "./src",  // srcにtsファイルを入れる
    "outDir": "./build",  // コンパイルしたファイルは./buildに配置
    "resolveJsonModule": true // JSONをimportできるようにする
  },
  "exclude": [
    "**/*.test.ts" // テストファイルはコンパイル対象から除外
  ]
}
```

ここの設定はお好みでいいと思います。


## コンパイルしてみる

この設定でコンパイルできるか試してみます。
ここで動かなかったら`tsconfig.json`の設定を見直します。

まずは`src`ディレクトリを作ります。
これは`rootDir`の設定が`./src`なのに合わせています。

そして、`src`ディレクトリの直下に`index.ts`を作ります。

```ts:src/index.ts
const text: string = 'Hello World!';
console.log(text);
```

ではコンパイルします。
以下のコマンドを入力してください。

```bash:ターミナル
tsc
```

すると、新しく`build`ディレクトリが作られ、その中にコンパイルした`index.js`ファイルが入ります。
だいたい以下のような中身になるはずです。

```js:build/index.js
"use strict";
const text = 'Hello World!';
console.log(text);
```

エラーが出ずに上のような結果になれば、ひとまずコンパイルは大丈夫だと思います。
あとはエラーが出た都度`tsconfig.json`を編集する感じです。


## 2のまとめ

1. `tsc --init`でファイルを生成する
1. 設定を書き換える
    <details><summary>私が使っている設定</summary>
    
    ```jsonc:tsconfig.json
    {
      "compilerOptions": {
        "target": "ES2022",
        "rootDir": "./src",
        "outDir": "./build",
        "resolveJsonModule": true
      },
      "exclude": [
        "**/*.test.ts"
      ]
    }
    ```
    
    </details>
1. `tsc`を実行してコンパイルできることを確認する


# 3. jest.config.jsを書き換える

次はJestの設定ファイルである`jest.config.js`を書き換えていきます。

ちなみに、この設定ファイルがない状態で`jest`コマンドを実行すると、以下のように怒られます。

```bash
jest
Error: Could not find a config file based on provided values:
略
```


## ファイルを生成する

以下のコマンドを入力します。

```bash:ターミナル
yarn init -y
warning The yes flag has been set. This will automatically answer yes to all questions, which may have security implications.
success Saved package.json
Done in 0.04s.

jest --init
? Would you like to use Jest when running "test" script in "package.json"? y
? Would you like to use Typescript for the configuration file? 
? Choose the test environment that will be used for testing
    > node
? Do you want Jest to add coverage reports? n
? Which provider should be used to instrument code for coverage?
    > v8
? Automatically clear mock calls, instances, contexts and results before every test? y
```

`yarn init -y`を実行しているのは、`package.json`を生成するためです。
このファイルがないと`jest --init`を実行した時にエラーが出て失敗してしまいます。
Warningが出ていますが、とりあえず気にしないことにします。

`jest --init`で設定した内容は以下のとおりです。

1. `test`スクリプトを`package.json`に追加するか？ yes
    * これをやると`yarn test`でテストが実行できるようになる
1. `jest.config.js`にTypeScriptを使うか？ no
    * これを`y`にしたら`ts-node`が必要って言われた記憶がある
    * JSでもJSDocコメントによって補完は出る
1. テスト環境はどうするか？ node
    * よくわからんけど`node`でいいと思う
1. カバレッジレポートは必要か？ no
    * よくわからんからnoにしてる
1. v8とbabelのどっちを使うか？ v8
    * これもよくわからん
1. テスト前にデータを消去するか？ yes
    * よくわからんが消していいと思う

:::note warn
多分これでも動くとは思いますが、一応自分で調べることを推奨します。
:::


## 必要なものをインストールする

現状だとテストができないため、必要なパッケージを入れます。


### 型定義を追加する

現状だと型定義がないため、`describe`などの関数を書こうとするとVSCode上でエラーが出てしまいます。
これを解消するため、`@types/jest`パッケージを入れます。

```bash:ターミナル
yarn add --dev @types/jest
```

これで補完が効いてエラーが出なくなるはずですが、一応試してみます。
というわけで、テストファイルを作ります。

`test`ディレクトリを新しく作り、その中に`sum.test.ts`を作ってください。

```ts:test/sum.test.ts
describe('test', () => {
    test('てすと', () => {
        expect(1 + 1).toBe(2);
    });
});
```

ここの内容はなんでもいいですが、`describe`を書いてエラーが出なければ多分大丈夫だと思います。


### importを使えるようにする

このJest、実はデフォルトのままだと`import`文が使えません。


#### エラーが発生することを確認する

試しに、`src/index.ts`と`test/sum.test.ts`を以下のようにします。

```ts:src/index.ts
// xとyの合計を返す関数
export function add(x: number, y: number) {
    return x + y;
}
```

```diff_typescript:test/sum.test.ts
+ import { add } from "../src/index";

  describe('test', () => {
      test('てすと', () => {
-         expect(1 + 1).toBe(2);
+         expect(add(1, 1)).toBe(2);
      });
  });
```

そして、以下のコマンドを実行します。

```bash:ターミナル
jest
 FAIL  test/sum.test.ts
  ● Test suite failed to run

    Jest encountered an unexpected token
    # 略

    SyntaxError: Cannot use import statement outside a module
```

見ての通り、「SyntaxError: Cannot use import statement outside a module」というエラーが出ました。
これは`import`がモジュールではない箇所で使われているというエラーです。


#### インストールする

このエラーを解決するためには、`ts-jest`というパッケージが必要です。
これはJestが内部で使う、TypeScriptをNode.jsで実行できるJavaScriptにするためのものです。

というわけで、以下のコマンドを実行してください。

```bash:ターミナル
yarn add --dev ts-jest

warning " > ts-jest@29.1.2" has unmet peer dependency "jest@^29.0.0".
warning " > ts-jest@29.1.2" has unmet peer dependency "typescript@>=4.3 <6".

# 略
```

上のようなWarningが出ますが、ひとまず気にしないことにします。

また、この`ts-jest`を使うためには`typescript`パッケージが必要です。
これは自動でインストールされないので、手動で入れていきます。

以下のコマンドを実行してください。

```bash:ターミナル
yarn add --dev typescript
```


## 設定ファイルを書き換える

ということで、`jest.config.js`を書き換えていきます。
といっても書き換えるのは`preset`のみです。

以下のようにファイルを編集してください。

```js:jest.config.js
/** @type {import('jest').Config} */
const config = {
    // 他の設定はデフォルトのままにしておく
    preset: "ts-jest"
};
```

これで、先ほどインストールした`ts-jest`パッケージを、Jestが内部で使うようになるはずです。


## テストしてみる

では、いよいよテストを実行してみたいと思います。

まず、`src/index.ts`と`test/sum.test.ts`が以下のようになっていることを確認してください。
多少違っても、`import`と`expect().toBe()`あたりが入っていれば大丈夫です。

```ts:src/index.ts
export function add(x: number, y: number) {
    return x + y;
}
```

```ts:test/sum.test.ts
import { add } from "../src/index";

describe('test', () => {
    test('てすと', () => {
        expect(add(1, 1)).toBe(2);
    });
});
```

確認ができたら、ターミナルに以下のコマンドを入力してください。

```bash:ターミナル
jest # yarn test でも可
 PASS  test/hello.test.ts
  test
    ✓ てすと (1 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.295 s
Ran all test suites.
```

こんな感じでエラーが出ずに実行できれば成功です。


## 3のまとめ

1. `yarn init -y`を実行して`package.json`を生成する
1. `jest --init`を実行して`jest.config.js`を生成する
    * 「Would you like to use Jest when running "test" script in "package.json"?」は`y`にしたほうが便利だと思う
    * 「Would you like to use Typescript for the configuration file?」を`y`にする場合、`ts-node`パッケージが追加で必要だった気がする
1. 必要なモジュールを入れる
    * `@types/jest` ... Jestの型定義
    * `ts-jest` ... TypeScriptで書いたテストを実行するために必要
    * `typescript` ... `ts-jest`のために必要
1. 設定ファイルを書き換える
    * `jest.config.js`の`preset`を`ts-jest`にする
1. `jest`コマンドでテストしてみる


# 参考

https://qiita.com/quotto/items/a672e8b3b45865832577