私のNodeJS+TypeScriptプロジェクト初期設定手順

# この記事について

NodeJS環境で実行するサーバサイドアプリケーション開発において
新規プロジェクト(リポジトリ)に TypeScript、フォーマッタ(Prettier)、リンタ(ESLint)、単体テストツール(Jest)を導入する手順のメモ
※適宜加筆修正を行っていく予定

# 手順

## package.json の生成

```sh
npm init -y
```

## TypeScript の導入

TypeScript本体とNodeJS組み込み機能の型定義パッケージをインストール

```sh
npm install --save-dev typescript @types/node
```

### tsconfig.json を設定

以下のコマンドで、コメント付きの tsconfig.json ファイルが生成されるのでいくつか設定を変更する

```sh
npx tsc --init
```

以下の設定例ではソースコードを src ディレクトリ、生成物を dist ディレクトリに配置する設定となっている

```json:tsconfig.json 設定例
{
    "compilerOptions": {
        "incremental": true,
        "tsBuildInfoFile": "./.tsbuildinfo",

        "target": "ES2021",
        "lib": ["ES2021"],
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true,

        "module": "commonjs",
        "moduleResolution": "node",
        "rootDir": "./src",
        "baseUrl": "./src",
        "paths": {
            "~/*": ["./*"]
        },
        "resolveJsonModule": true,
        "allowJs": true,
        "checkJs": true,
        "outDir": "./dist",

        "isolatedModules": true,
        "esModuleInterop": true,
        "forceConsistentCasingInFileNames": true,

        "strict": true,
        "noUnusedLocals": true,
        "noUnusedParameters": true,
        "exactOptionalPropertyTypes": true,
        "noImplicitReturns": true,
        "noFallthroughCasesInSwitch": true,
        "noUncheckedIndexedAccess": true,
        "noImplicitOverride": true,
        "noPropertyAccessFromIndexSignature": true,
        "allowUnusedLabels": false,
        "allowUnreachableCode": false,

        "skipLibCheck": true
    },
    "include": ["./src"],
    "exclude": ["node_modules", "./dist"]
}
```

<details> 
<summary>tsconfig.json 各設定値について補足</summary>

> "target": "ES2021",
> "lib": ["ES2021"],

NodeJSのバージョンに合わせて設定、(現在LTSの)NodeJS v16では ES2021 までサポートしている

> "experimentalDecorators": true,
> "emitDecoratorMetadata": true,

デコレータを利用する必要がある場合 true にしておく

> "rootDir": "./src",

ソースコードを src ディレクトリ内に置くプロジェクトレイアウトの場合はこのようにしておく

> "baseUrl": "./src",
> "paths": { "~/*": ["./*"] },

パスエイリアスの設定。どの階層のファイルからも src/ を ~/ と見做して import できるようにしている

> "outDir": "./dist",

コンパイルした JS の出力先。
ソースコードと同じディレクトリに混在しないよう　dist ディレクトリ配下に出力する設定にする
webpack や esbuild などのバンドルツールを利用する場合は効果がないので注意

> "strict": true,
> から
> "allowUnreachableCode": false,

これらの厳密な構文チェックの設定は最初にすべて有効にしておき、支障がある場合のみ無効化している
</details>

各項目のさらに詳しい説明は[公式ドキュメント](https://www.typescriptlang.org/tsconfig)を参照

### TypeScriptでの開発中に簡易に実行するためのツールを追加

```sh
npm install --save-dev ts-node ts-node-dev tsconfig-paths
```

[ts-node](https://www.npmjs.com/package/ts-node) …TSファイルを直接実行できる
[ts-node-dev](https://www.npmjs.com/package/ts-node-dev) …(サーバプログラム開発などで)実行しながらコード修正時に自動で再起動(watch)させる
[tsconfig-paths](https://www.npmjs.com/package/ts-node-dev) …実行時にパスエイリアスを解決させる
を大抵の場合TypeScriptのプロジェクトに追加している

以下のように `-r tsconfig-paths/register` を渡すことでパスエイリアスを解決させて実行できる

```sh
ts-node -r tsconfig-paths/register src/main.ts
ts-node-dev -r tsconfig-paths/register src/main.ts

# パスエイリアスを使用したTSをコンパイルしたJS実行時に
TS_NODE_BASEURL=dist node -r tsconfig-paths/register dist/main.js
```

### package.json の scripts に追加

短いコマンド `npm run dev` でメインスクリプトを起動できるよう package.json の scripts に登録しておく

```diff:package.json
  "scripts": {
      ...
+     "dev": "ts-node-dev -r tsconfig-paths/register src/main.ts",
      ...
  },
```

## フォーマッタ(Prettier)の導入

フォーマッタツール[Prettier](https://prettier.io/)をプロジェクトに追加する

```sh
npm install --save-dev prettier
```

### .prettierrc.js を設定

プロジェクトの規約に沿って設定値を変える

```js:.prettierrc.js雛形
/** @type {import("prettier").Config} */
module.exports = {
    printWidth: 120,
    tabWidth: 4,
    useTabs: false,
    semi: true,
    singleQuote: false,
    quoteProps: "consistent",
    jsxSingleQuote: false,
    trailingComma: "all",
    bracketSpacing: true,
    bracketSameLine: false,
    arrowParens: "always",
};
```

<details>
<summary>各設定項目の補足</summary>

```js:.prettierrc.js
/** @type {import("prettier").Config} */
module.exports = {
    // 1行の最大長
    printWidth: 120,
    // インデント幅
    tabWidth: 4,
    // インデントにスペースを使用
    useTabs: false,
    // 文末にセミコロンをつける
    semi: true,
    // 文字列リテラルをダブルクォーテーションで囲む
    singleQuote: false,
    // オブジェクトリテラルのプロパティ名のクォーテーションに一貫性をもたせる
    quoteProps: "consistent",
    // JSX属性値をダブルクォーテーションで囲む
    jsxSingleQuote: false,
    // 配列(オブジェクト)リテラルの最後の要素(プロパティ)の後ろにもカンマを付ける
    trailingComma: "all",
    // 波括弧の内側にスペースを入れる
    bracketSpacing: true,
    // 複数行のHTML要素・JSXタグの「>」を単独で新しい行に置く
    bracketSameLine: false,
    // アロー関数の引数が1つの場合も丸括弧で囲む
    arrowParens: "always",
};
```

※更に詳しい各設定項目の説明は[公式ドキュメント](https://prettier.io/docs/en/options.html)を参照

</details>

### package.json の scripts を追加

チェックのみを行う `lint` スクリプトと、訂正を行う `fix`スクリプトを package.json に追加しておく

```diff:package.json
  "scripts": {
      ...
+     "lint": "prettier -l src",
+     "fix": "prettier -w src",
      ...
  }
```

## テストツール (Jest) の導入

[Jest](https://jestjs.io/ja/)をプロジェクトに追加する
あわせて TypeScript に対応させるための [ts-jest](https://www.npmjs.com/package/ts-jest) と型定義も追加

```sh
npm install --save-dev jest @types/jest ts-jest
```

### 設定ファイル jest.config.js の追加

TypeScript用の設定ファイルを生成する

```sh
npx ts-jest config:init
```

生成された jest.config.js を、
パスエイリアスに対応させるため以下のように修正

```js:jest.config.js
const { pathsToModuleNameMapper } = require("ts-jest");
const { loadConfig } = require("tsconfig-paths");

const tsconfig = loadConfig();

/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
    preset: "ts-jest",
    testEnvironment: "node",

    // paths
    moduleNameMapper: pathsToModuleNameMapper(tsconfig.paths, {
        prefix: tsconfig.absoluteBaseUrl,
    }),
};
```

### package.json の scripts を追加

`npm run test` で実行できるように package.json に追加しておく
(オプションを追加しなければ `npx jest` より長くなるのであまり意味がないかもしれない…)

```diff:package.json
  "scripts": {
      ...
+     "test": "jest",
      ...
  }
```

## コードチェックツール ESLint の導入

[ESLint](https://eslint.org/)を追加

TypeScriptに対応させるための [typescript-eslint](https://www.npmjs.com/package/@typescript-eslint/eslint-plugin)
import 順序のルールを追加する [eslint-plugin-import](https://www.npmjs.com/package/eslint-plugin-import)
prettier と競合する　eslint ルールを無効化する [eslint-config-prettier](https://www.npmjs.com/package/eslint-config-prettier)
jest 用のルールを追加する [eslint-plugin-jest](https://www.npmjs.com/package/eslint-plugin-jest) なども追加する

```sh
npm install --save-dev eslint \
  @typescript-eslint/parser @typescript-eslint/eslint-plugin \
  eslint-plugin-import eslint-import-resolver-typescript \
  eslint-config-prettier eslint-plugin-jest
```

### 設定ファイル .eslintrc.js を追加

以下のように各プラグインの推奨ルールを extends に入れておき、
プロジェクト開発に合わないものや足りないものを　rules で無効化/追加する

```js:.eslintrc.js
/** @type {import("eslint").Linter.Config} */
module.exports = {
    env: {
        node: true,
        commonjs: true,
        es2021: true,
        jest: true,
    },
    plugins: [
        //
        "@typescript-eslint",
        "import",
        "jest",
    ],
    parser: "@typescript-eslint/parser",
    parserOptions: {
        project: "./tsconfig.json",
    },
    settings: {
        "import/parsers": {
            "@typescript-eslint/parser": [".ts", ".tsx"],
        },
        "import/resolver": {
            typescript: true,
        },
    },
    extends: [
        "eslint:recommended",
        "plugin:@typescript-eslint/recommended",
        "plugin:import/recommended",
        "plugin:import/typescript",
        "plugin:jest/recommended",
        "prettier",
    ],
    rules: {},
};
```

<details>
<summary>rulesに追加する一例</summary>

ESLintrules追加の例

```js:.eslintrc.jsのrules抜粋
// 単一行のifブロックの波括弧省略禁止
"curly": "error",

// ifブロックで必ずreturnする場合のelse記述禁止
"no-else-return": "error",

// 厳密比較の強制
"eqeqeq": "error",

// 関数の最大行数(トップレベルの関数除く)
"max-statements": ["error", 40, { ignoreTopLevelFunctions: true }],

// ひとつの var/const/letで複数の変数を宣言することを禁止
"one-var": ["error", "never"],
```

import のルール追加例

```js:.eslintrc.jsのrules抜粋
    // import文をファイルの先頭に置く
    "import/first": "error",

    // import 記述順のルール
    "import/order": ["error", {
        // アルファベット順に並べる
        "alphabetize": { order: "asc", caseInsensitive: false },
        // グループごとに並べる NodeJSビルトインモジュール→外部モジュール→内部モジュール→親ディレクトリ
        "groups": ["builtin", "external", "internal", "parent", "sibling"],
        // グループ間に空行を入れる
        "newlines-between": "always",
    }],

```

</details>

### package.json の scripts を修正

`npm run lint` で ESLint によるチェックも実行されるようにする
`npm run fix` で ESLint ルール違反の訂正(自動でできるものに限る)も実行されるようにする

```diff:package.json
  "scripts": {
      ...
-     "lint": "prettier -l src",
+     "lint": "prettier -l src && eslint src",
-     "fix": "prettier -w src",
+     "fix": "prettier -w src && eslint src --fix",
      ...
  }
```