かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

TypeScrip + ESLint + Prettier + Jest のプロジェクト設定にハマる

新規に作ろうとして環境作るまでに盛大にハマったのでメモ

環境

"typescript": "^4.3.5"
"eslint": "^7.30.0",
"prettier": "^2.3.2",
"jest": "^27.0.6",

最終的にできたもの

TL:DR;

TypeScript + ESLint+ Prettier の環境を作る

TypeScript

$ npm i --save-dev typescript 
$ npx tsc --init

tsconfig.json

{
  "compilerOptions": {
    "target": "es5" ,
    "module": "commonjs,
    "lib": [ "dom", "ES2017"],
    // `.d.ts` ファイルを生成
    "declaration": true,
    // sourcemap の `.d.ts` ファイルを生成
    "declarationMap": true,
    "sourceMap": true,
    "strict": true,
    // 使われているthisの型が暗黙的にanyになる場合にエラー
    "noImplicitThis": true,
    // 宣言されたが使用されていない変数が存在する場合にエラー
    "noUnusedLocals": true,
   // 条件分岐の条件によって明示的なreturnがされないルートがあるとエラー
   "noImplicitReturns": true,
   "esModuleInterop": true,
   // Enables experimental support for ES7 decorators.
   "experimentalDecorators": true, 
   // Enables experimental support for emitting type metadata for decorators.
   "emitDecoratorMetadata": true,
   "skipLibCheck": true,
   "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*", "tests/**/*"],
  "exclude": ["node_modules"]
}

Build の設定

Build 自体は npx tsc --project . で実行できる
commonJS, esNext それぞれでビルドできるように npm script を追加する

package.json

  "scripts": {
+   "build:cjs": "tsc --project . --module commonjs --outDir ./dist-cjs",
+   "build:esm": "tsc --project . --module esNext --outDir ./dist-esm",
+   "build": "npm run build:cjs && npm run build:esm"
  }

ESLint + Prettier

ESLint + Prettier を実行する npm script を追加

package.json

{
  "scripts": {
    "build:cjs": "tsc --project . --module commonjs --outDir ./dist-cjs",
    "build:esm": "tsc --project . --module esNext --outDir ./dist-esm",
    "build": "npm run build:cjs && npm run build:esm",
+   "lint": "eslint \"src/**/*.{js,ts}\"",
+   "lint:fix": "eslint --fix \"src/**/*.{js,ts}\"",
+   "format": "prettier --write \"src/**/*.{js,ts}\""
  }
}

必要なパッケージのインストール

$ npm i --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
$ npm i --save-dev prettier eslint-config-prettier
$ touch .prettierrc
$ npx eslint --init

.eslintrc.js

module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true,
  },
  extends: [
    'airbnb-base',
    // 型を必要としないプラグインの推奨ルールをすべて有効
    'plugin:@typescript-eslint/recommended',
    // 型を必要とするプラグインの推奨ルールをすべて有効
    'plugin:@typescript-eslint/recommended-requiring-type-checking',
    'prettier',
    // ESLint で Prettier の規則もエラーとして検出する設定
    'prettier/@typescript-eslint',
  ],
  plugins: ['@typescript-eslint'],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 12,
    sourceType: 'module',
  },
  rules: {
    'newline-before-return': 'error',
    'no-console': 'warn',
    'no-var': 'error',
  },
};

.prettierrc

{
  "printWidth": 120,
  "tabWidth": 2,
  "semi": true,
  "singleQuote": true,
  "trailingComma": "es5",
  "arrowParens": "always",
  "useTabs": false,
  "endOfLine": "lf"
}

VS Code で保存時に ESLint + Prettier でフォーマットする設定

$ mkdir .vscode
$ touch .vscode/settings.json

.vscode/settings.json

{
  editor.tabSize": 2,
  "eslint.validate": [
    "javascript",
    "typescript",
  ],
  // ファイル保存時の自動フォーマット有効
  "editor.formatOnSave": true,
  // デフォルトフォーマッターをPrettierに指定
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.codeActionsOnSave": {
    // ファイル保存時に ESLint でフォーマット
    "source.fixAll.eslint": true
  },
  "[markdown]": {
    "files.trimTrailingWhitespace": false
  }
}

TypeScript + ESLint の設定エラー

Error while loading rule '@typescript-eslint/await-thenable': You have used a rule which requires parserServices to be generated. You must therefore provide a value for the "parserOptions.project" property for @typescript-eslint/parser.

ESLint の parserOptions.project に明示的に tsconfig ファイルを指定する必要がある

.eslintrc.js

  plugins: ['@typescript-eslint'],
  parser: '@typescript-eslint/parser',
  parserOptions: {
-  ecmaVersion: 12,
-  sourceType: 'module',
+  project: './tsconfig.json',
  },

cf.

.eslintrc.js 編集中に VS Code が延々エラーを表示してくる

Parsing error: "parserOptions.project" has been set for @typescript-eslint/parser. The file does not match your project config: .eslintrc.js. The file must be included in at least one of the projects provided.

.eslintrc.js も監視対象になっていてプロジェクトに使われてないという警告。
.eslintrc.js は設定ファイルでプロジェクトに使わないので対象外にする。

$ touch .eslintignore

.eslintignore

/.eslintrc.js

cf. [VSCode] TypeScriptにESLintを入れると Parsing error: “parserOptions.project” has been set for @typescript-eslint/parser. が表示される │ wonwon eater

Prettier + ESLint の設定エラー Error: "prettier/@typescript-eslint" has been merged into "prettier" in eslint-config-prettier 8.0.0.

Version 8.0.0 (2021-02-21) から ESLint の extends に書く prettier/@typescript-eslint の指定は prettier のみで OK になった

.eslintrc.js

  extends: [
    'airbnb-base',
    'plugin:@typescript-eslint/recommended',
    'plugin:@typescript-eslint/recommended-requiring-type-checking',
    'prettier',
-   'prettier/@typescript-eslint',
  ],

cf. eslint-config-prettier/CHANGELOG.md at main · prettier/eslint-config-prettier · GitHub

ESLint のルール設定

airbnb-base を入れたので発生している可能性あり

named export がエラーになる Prefer default export import/prefer-default-export

export const MyModule = () => {};
// => ESLint: Prefer default export  import/prefer-default-export

import/prefer-default-export オプションを OFF にする

.eslintrc.js

  rules: {
+   'import/prefer-default-export': 'off',
    'newline-before-return': 'error',
    'no-console': 'warn',
    'no-var': 'error',
  },

import 時に .ts の拡張子が無いとエラー Error: error Missing file extension for "./MyModule" import/extensions

.ts ファイルが拡張子なしでも import できる設定が必要

.eslintrc.js

  parserOptions: {
    project: './tsconfig.json',
  },
+ settings: {
+   'import/extensions': ['.js', '.jsx', '.ts', '.tsx'],
+   'import/parsers': {
+     '@typescript-eslint/parser': ['.ts', '.tsx'],
+   },
+   'import/resolver': {
+     node: {
+       extensions: ['.js', '.jsx', '.ts', '.tsx'],
+     },
+   },
+ },
  rules: {
+   'import/extensions': [
+     'error',
+     'ignorePackages',
+     { js: 'never', jsx: 'never', ts: 'never', tsx: 'never' },
+   ],
    'import/prefer-default-export': 'off',

TypeScript のコンパイル設定

スプレッド構文 (e.g. [..."STRING"]) でエラー Type 'XXXXX' is not an array type or a string type. Use compiler option '--downlevelIteration' to allow iterating of iterators.

[..."STRING👻"][...document.querySelectorAll('span')] のような書き方を許可するには downlevelIteration オプションが必要

tsconfig.json

{
  "compilerOptions": {
    "target": "es5" ,
    "module": "commonjs,
    // target が es6 以下の場合は "dom.iterable" が必要
-   "lib": [ "dom", "ES2017"],
+  "lib": [ "dom", "dom.iterable", "ES2017"],
+   "downlevelIteration": true,

Jest

Jest でテストを実行するための npm script を追加

package.json

  "scripts": {
    "build:cjs": "tsc --project . --module commonjs --outDir ./dist-cjs",
    "build:esm": "tsc --project . --module esNext --outDir ./dist-esm",
    "build": "npm run build:cjs && npm run build:esm",
+   "test": "jest --runInBand",
+   "test:coverage": "jest test --coverage",
    "lint": "eslint \"src/**/*.{js,ts}\"",
    "lint:fix": "eslint --fix \"src/**/*.{js,ts}\"",
    "format": "prettier --write \"src/**/*.{js,ts}\""
  }

パッケージのインストール

$ npm i --save-dev jest ts-jest @types/jest
$ npx jest --init

jest.config.js

module.exports = {
  clearMocks: true,
  coverageProvider: 'v8',
  testEnvironment: "node",
  moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json', 'node'],
  roots: ['<rootDir>/tests/'],
  transform: { '^.+.(js|jsx|ts|tsx)$': 'ts-jest' },
  transformIgnorePatterns: ['/node_modules/', '\\.pnp\\.[^\\/]+$'],
}

transform ではなく preset: 'ts-jest' で TypeScript を有効にする

公式の Basic usage に従って preset: 'ts-jest' を指定する方法に変更する
これを指定すると、よしなに transform してくれるっぽい

jest.config.js

module.exports = {
  clearMocks: true,
  coverageProvider: 'v8',
+ preset: "ts-jest",
  testEnvironment: "node",
  moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json', 'node'],
  roots: ['<rootDir>/tests/'],
- transform: { '^.+.(js|jsx|ts|tsx)$': 'ts-jest' },
  transformIgnorePatterns: ['/node_modules/', '\\.pnp\\.[^\\/]+$'],
}

cf.

Build 時にテストファイルを除く

この状態でビルドすると /tests ディレクトリも含まれてしまうのでテストファイルはビルドから除外する。
ビルド用の設定ファイルを作成しビルド時はその設定を使うようにする

$ touch tsconfig.build.json

tsconfig.build.json

{
  "extends": "./tsconfig.json",
  "exclude": ["tests/**/*", "**/?(*.)+(spec|test).[tj]s?(x)"]
}

ビルド時に tsconfig.build.json を使ってビルドするように変更

package.json

  "scripts": {
-   "build:cjs": "tsc --project . --module commonjs --outDir ./dist-cjs",
+   "build:cjs": "tsc --project tsconfig.build.json --module commonjs --outDir ./dist-cjs",
-   "build:esm": "tsc --project . --module esNext --outDir ./dist-esm",
+   "build:esm": "tsc --project tsconfig.build.json --module esNext --outDir ./dist-esm",
    "build": "npm run build:cjs && npm run build:esm"
  }

cf. typescript - Setting up tsconfig with spec/test folder - Stack Overflow

感想

まとめてたらめっちゃ時間掛かってしまった…
久しぶりに環境作ろうと思ったらエラーになりまくるのホントどうにかして欲しい…

と思ってテンプレートのリポジトリ作った


[参考]

この記事はサガみたいに LOOOOOONG !!!