快適な TypeScript 開発環境を作る for VS Code

VS Code を使って TypeScript 言語ベースの開発をよくしますが、「アイデアを実現するコードを紡ぎ出す」という命題を邪魔してくる面倒ごとがまま出てくる。

使われてない import の山々や変数、微妙に不揃いなコーディングスタイル。
そして書いた覚えのないタイポ。Paramater と打ち込んでコミットされたコードを何度修正した事か…。

そんな面倒ごとを極力自動でお守してくれる快適環境を目指します。

# 前提

* Node.js がインストールされていること。
* VS Code がインストールされていること。

パッケージ管理に npm を使っていますが、yarn や pnpm でも転用できると思います。

# この構成でできること

ファイル保存時に自動適用される主な処理です。

* コード自体の調整
    * import するパッケージ名・ファイル名のアルファベット順で並び替えます
    * import はしているが未使用の関数やクラス等を消します
    * 未使用の import を消します
* コーディングスタイルの調整
    * 改行コードを LF に統一します
    * インデントは 4 スペースで揃えます (JSON と Markdown の場合は 2 スペース)
    * 行末の余分な空白を消します (Markdown の場合は残します)
    * ステートメントの最後にはセミコロンを付けます
    * 文字列の囲い文字はダブルクオートを基本にします
    * if や for などの構文では括弧の前後に空白を挿入します
    * アロー関数の引数などで、省略できる括弧でも括ります

指摘される主な記述です。

* コードの指摘
    * [列挙型 (enum)](https://typescriptbook.jp/reference/values-types-variables/enum/enum-problems-and-alternatives-to-enums) の使用
    * 関数の引数で受け取っているが未使用
        * アンダースコア '_' で始まる場合と、インターフェイスの記述では例外
    * any の使用
* 単語の指摘
    * 一般的な英単語にない
    * 独自の単語辞書に登録されてない

そのほか、パスエイリアス (paths alias) も設定しています。

# VS Code

4 種類の拡張機能をインストールします。これは VS Code 自体に追加するので作業は 1 回のみです。

## インストールする拡張機能

* [EditorConfig for VS Code](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig)
* [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
* [Prettier - Code formatter](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
* [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker)

### コマンドで入れる

PowerShell などで以下のコマンドを順次実行させれば拡張機能をインストールできます。

```powershell
code --force --install-extension EditorConfig.EditorConfig
code --force --install-extension dbaeumer.vscode-eslint
code --force --install-extension esbenp.prettier-vscode
code --force --install-extension streetsidesoftware.code-spell-checker
```

# Node のプロジェクトを作る

ここからはプロジェクトごとの作業です。

## インストールする npm パッケージ

* TypeScript 関連
    * typescript
    * @types/node
    * ts-node
    * tsconfig-paths
* 記述スタイルの整形
    * prettier
* コーディングルール
    * eslint
    * eslint-config-prettier
    * @typescript-eslint/parser
    * @typescript-eslint/eslint-plugin
* ESLint プラグイン関連
    * eslint-plugin-import
    * eslint-plugin-unused-imports
    * eslint-plugin-typescript-enum
* 単語チェック＆訂正
    * cspell
* よく使う便利系
    * npm-run-all
    * rimraf

### コマンド

```powershell
mkdir example-project
cd example-project

npm init -y

npm i -D typescript @types/node ts-node tsconfig-paths
npm i -D prettier eslint eslint-config-prettier @typescript-eslint/parser @typescript-eslint/eslint-plugin
npm i -D eslint-plugin-import eslint-plugin-unused-imports eslint-plugin-typescript-enum
npm i -D cspell npm-run-all rimraf
```

## 設定ファイル

プロジェクト直下のディレクトリ (`example-project`) 内に作るファイルたちです。

### TypeScript

`tsconfig.json` はビルド時や実行時はもちろん、__VS Code の ESLint 拡張機能__ からも参照されます。tsconfig.json には全体のルールのみ設定しておき、ビルド対象などの定義は別ファイルにします。

```json:tsconfig.json
{
  "compilerOptions": {
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "alwaysStrict": true,
    "baseUrl": "./",
    "checkJs": true,
    "declaration": false,
    "declarationMap": false,
    "emitDecoratorMetadata": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "forceConsistentCasingInFileNames": false,
    "incremental": false,
    "inlineSourceMap": false,
    "inlineSources": false,
    "isolatedModules": true,
    "lib": ["DOM", "DOM.Iterable", "ESNext"],
    "listEmittedFiles": false,
    "module": "commonjs",
    "moduleResolution": "node",
    "newLine": "lf",
    "noFallthroughCasesInSwitch": true,
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "outDir": "./dist",
    "paths": {
      "@/*": ["./src/*"]
    },
    "preserveConstEnums": true,
    "removeComments": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "strict": true,
    "strictBindCallApply": false,
    "strictNullChecks": true,
    "strictPropertyInitialization": false,
    "target": "es2020"
  }
}
```

```json:tsconfig.build.json
{
  "extends": "./tsconfig.json",
  "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}
```

```json:tsconfig.types.json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "removeComments": false,
    "declaration": true,
    "declarationDir": "./dist/types",
    "emitDeclarationOnly": true
  },
  "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}
```

### package.json

npm スクリプトと設定抜粋です。

* `npm run bootstrap` でルールチェックを完全パスした後にクリーンビルドをして成果物を作れます。
* `npm run start` で成果物（JavaScriptとして）で実行します。
* `npm run dev` で TypeScript のソースを直接実行します。

```json:package.json
{
  "module": "commonjs",
  "main": "./dist/index.js",
  "types": "./dist/types/index.d.ts",
  "scripts": {
    "start": "node .",
    "dev": "ts-node -r tsconfig-paths/register ./src/index.ts",
    "clean": "rimraf dist",
    "lint": "run-s lint:*",
    "lint:cspell": "cspell lint --gitignore --no-progress src/**/*.ts",
    "lint:eslint": "eslint src/**/*.ts",
    "lint:prettier": "prettier --check src/**/*.ts",
    "build": "run-p build:*",
    "build:cjs": "tsc -p ./tsconfig.build.json",
    "build:types": "tsc -p ./tsconfig.types.json",
    "bootstrap": "run-s clean lint build"
  }
}
```

### 整形とコーディングルール関連

```ini:.editorconfig
root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

[*.json]
indent_size = 2

[*.md]
indent_size = 2
trim_trailing_whitespace = false
```

```json:.prettierrc.json
{
  "$schema": "http://json.schemastore.org/prettierrc",
  "arrowParens": "always",
  "bracketSameLine": false,
  "bracketSpacing": true,
  "endOfLine": "lf",
  "insertPragma": false,
  "jsxSingleQuote": false,
  "printWidth": 100,
  "proseWrap": "always",
  "quoteProps": "as-needed",
  "requirePragma": false,
  "semi": true,
  "singleQuote": false,
  "tabWidth": 4,
  "trailingComma": "es5",
  "useTabs": false,
  "overrides": [
    {
      "files": ["*.json"],
      "options": { "tabWidth": 2 }
    },
    {
      "files": ["*.md"],
      "options": { "tabWidth": 2, "proseWrap": "preserve" }
    }
  ]
}
```

```json:.eslintrc.json
{
  "$schema": "http://json.schemastore.org/eslintrc",
  "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
  "plugins": ["@typescript-eslint", "import", "unused-imports", "typescript-enum"],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "sourceType": "module",
    "ecmaVersion": 2020
  },
  "root": true,
  "env": {
    "node": true,
    "jest": true
  },
  "rules": {
    "@typescript-eslint/no-inferrable-types": "off",
    "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
    "@typescript-eslint/no-use-before-define": "error",
    "no-console": "off",
    "no-restricted-syntax": [
      "error",
      {
        "selector": "TSEnumDeclaration",
        "message": "Don't declare enums"
      }
    ],
    "no-unused-vars": "off",
    "no-use-before-define": "off",
    "import/order": [
      "warn",
      {
        "groups": ["builtin", "external"],
        "alphabetize": { "order": "asc" },
        "newlines-between": "always"
      }
    ],
    "import/newline-after-import": "warn",
    "unused-imports/no-unused-imports": "warn",
    "typescript-enum/no-enum": "error"
  }
}
```

### 単語チェック関連

```json:cspell.json
{
  "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json",
  "version": "0.2",
  "allowCompoundWords": false,
  "dictionaries": ["cspell-dictionary"],
  "dictionaryDefinitions": [
    {
      "name": "cspell-dictionary",
      "path": "./cspell-dictionary.txt",
      "scope": "workspace",
      "addWords": true
    }
  ]
}
```

独自の辞書ファイルにはプロジェクト名など固有名詞を予め入れておくのが良いです。

```cspell-dictionary.txt
# dictionary
hoge
fuga
```

#### 単語のメンテナス

不明な単語の指摘があった際は、以下のどちらかの操作で cspell-dictionary.txt ファイルに追加してくれます。

* クイックフィックスにある `Add "対象単語" to dictionary: cspell-dictionary (Workspace)` を選ぶ。
* 単語の上で右クリックして「Spelling ＞ Add Words to Workspace Dictionary」を選ぶ。

### VS Code 用の設定

ファイル保存時に以下の動作になります。

* `source.fixAll.eslint` で ESLint のルールで指摘されているクイックフィックスが実行されます。
* 言語モード固有設定の `editor.defaultFormatter` で指定したフォーマッタ (Prettier - Code formatter) が実行されます。
* EditorConfig の適用
    * 改行コードは Prettier にもあって競合している。が、どうも EditorConfig の方が効いてるっぽい？

```json:.vscode/settings.json
{
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true,
  "editor.formatOnSaveMode": "file",
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[typescriptreact]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[javascriptreact]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[json]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[jsonc]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[markdown]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  }
}
```

#### 補足

editor.defaultFormatter がデフォルトと各言語モードごとで重複指定になっていますが、現時点で試した結果これが一番でした。というのも、デフォルトで全部効くかと思いきや ts ファイル等は保存時に全く効かなかったのとドキュメントフォーマット (Shift+Alt+F) を指示しても効きませんでした。 `[typescript]` などの言語モードごとのセクションで指定すれば効きました。

よく使う言語モードは上述の設定で列挙しました。他のモードについては以下の手順で追加するのが楽です。

1. VS Code でファイルを開く。
1. 右クリック「ドキュメントのフォーマット...」で選択肢を表示する。
1. 「既定のフォーマッタを構成...」を選択する。
1. 「Prettier - Code formatter」を選択する。

これで settings.json に追加されます。

※デフォルトの editor.defaultFormatter にフォーマッタが何てあれ設定されてないと追加されないようです。

# 最初のコード

ここまで出来ればソースを書いていけます。

```src/index.ts
function say(name: string) {
    console.log(`ようこそ！ ${name} さん`);
}

(() => {
    say("サンプル");
})();
```

## 実行方法 1

TypeScript で直接実行してみる。

```
npm run dev
```

## 実行方法 2

ビルドしてから JavaScript として実行してみる。

```
npm run build
npm run start
```

## ルールチェック

全てのソースがルールに従って記述されてるかチェックする。

```
npm run lint
```

個別にするなら `npm run lint:cspell` など。

# 最後に

ここまで設定すれば TypeScript 言語のコードを実行するという初歩を超えて、腰を据えて TypeScript としっかり向き合って開発できる環境が整ったと思います。

既にお気づきかと思いますが、テストコード部分が丸っとありません。もうお腹いっぱいなので、それは別の機会に。

## 参考

https://qiita.com/mima_ita/items/579fcc3a65cee4ef9b01

https://typescriptbook.jp/