typescriptをbabelでビルドしつつlintや型チェックもやりたい

(2019/04/14追記)
tslint-loaderのtypecheckオプションを付けるとビルドが大幅に遅くなるようです。
しかし、typecheckをtrueにしておかないとビルド時にwarningが出ます。
https://github.com/wbuchwalter/tslint-loader/issues/76

tslint以外を使用するか、ビルド時にはlinterチェックはせず、pre-commitのタイミングでlinterを単体で走らせるようにしたほうが良さそうです。
この記事に関してもtslint-loader関連の部分は削除しました。

## はじめに
babel7がリリースされましたね！
それに伴いtypescriptもサポートされたのでbabel単体でtsのビルドができるようになりました。
しかしbabelはtypescriptから型情報を取り除くだけなので型チェックまではサポートしておらず、
型チェックに関してはtsc経由で行う必要あります。
今回はwebpack + babel + tsc + tslintでtypescriptのビルド＋lint+型チェックを行えるようにしたいと思います。

※ソースはこちらです
https://github.com/tmzkysk/hello_typescript

## 必要なmoduleをインストール
まずは適当に作業ディレクトリを作り、
npm(yarn) initでプロジェクトを作成してください。
※yarnを入れていない人は随時npmで置き換えてください。

```sh
$ mkdir hello_typescript && cd hello_typescript
$ yarn init
```

package.jsonが作成されたら必要なmoduleをインストールしていきます。

```sh
$ yarn add typescript webpack webpack-cli webpack-dev-server
$ yarn add -D @babel/core @babel/preset-env babel-loader ts-loader tslint prettier tslint-config-prettier tslint-config-standard tslint-plugin-prettier
```

ひとつずつ解説すると、

- webpack：言わずもがな。ファイルバンドリングツール。
- webpack-cli: webpackをcli上で使用できるようにする。
- webpack-dev-server：開発時にtsの変更をwatchして自動的にビルドする
- @babel/core：ES6 -> ES5を行うトランスパイラ
- @babel/preset-env:サポートされている環境に基づいて必要なBabelプラグインを自動で決定するライブラリ
- babel-loader：webpack上でbabelを使用するために必要
- ts-loader：webpack上でtypescriptのビルドをするのに必要
- tslint：typescriptの構文チェッカー（型チェックはやらないよ）
- prettier：コードフォーマッター
- tslint-config-prettier：TSLintの設定のうちPrettierと衝突するものを無効化してくれるパッケージ
- tslint-plugin-prettier：TSLintのチェックをかけるときに一緒にPrettierをながしてくれる
- tslint-config-standard：[こちら](https://standardjs.com/)の内容にそってTSLintのチェックをするときに必要

結構多い。。。
TSLintとprettierの違いはこちらの記事が参考になります。
https://qiita.com/soarflat/items/06377f3b96964964a65d


## typescript側の設定
### tsconfig.jsonの作成
typescriptのビルドやチェックのルールを設定するファイルです。
以下のコマンドでディレクトリに作成されます。

```sh
$ yarn tsc --init
```

### tsconfig.jsonの設定
configに設定できる項目はたくさんありますが、とりあえずこれらを設定しておけば良いと思います。
型チェックや不要な変数のチェックなどはtsconfig側の設定になります。

```js:tsconfig.json
{
  "compilerOptions": {
    /* Basic Options */
    "target": "ESNEXT",                    /* ｔｓを書くときにどのversionのESが対象か: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
    "module": "ESNext",                    /* どのversionのESを生成するか: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
    "jsx": "preserve",                     /* reactを書くときに必要, 'react-native', or 'react'. */
    // "outFile": "./",                    /* 出力するファイル(ファイル出力はwebpackで行うので不要) */
    // "outDir": "./",                     /* 出力するディレクトリ(ファイル出力はwebpackで行うので不要) */
    // "rootDir": "./",                    /* ビルド時のルートディレクトリ(ビルド対象のパス設定はwebpackで行うので不要) */
    // "noEmit": "true",                   /* ビルド時に型ファイルを生成しない(これがあるとts-loaderでエラーが出るので外しています */

    /* Strict Type-Checking Options */
    "strict": true,                        /* すべての strict タイプ・オプション(noImplicitAny, strictNullChecks, noImplicitThis(thisの型チェック), alwaysStrict(strictモードのjs出力)) を 有効化する */
    "noImplicitAny": true,                 /* any型の使用不可 */
    "strictNullChecks": true,              /* nullable型以外でnullを許容しない */

    /* Additional Checks */
    "noUnusedLocals": true,                /* 未使用の変数を許容しない */
    "noUnusedParameters": true,            /* 未使用の変数を許容しない */
    "noImplicitReturns": true,             /* メソッド内で返り値の型があっているかをチェック */

    /* Module Resolution Options */
    "moduleResolution": "node",            /* http://js.studio-kingdom.com/typescript/handbook/module_resolution 参照 */
    "esModuleInterop": true                /* ESModuleと同じ動作をする. */
  }
}
```

## babelの設定
まずは.babelrcを作成します。

```sh
$ touch .babelrc
```

中身はこのような感じです。
(react開発をする場合は`@babel/preset-react`が必要です)

```json:.babelrc
{
  "presets": [
    "@babel/preset-env"
  ]
}
```

## tslintの設定
まずは設定ファイルを作ります。

```sh
$ touch tslint.json
```

中身はこのような感じ。
(reactの文法もチェックする場合は`tslint-react`が必要です）

```js:tslint.json
{
  "rulesDirectory": [
    "tslint-plugin-prettier"
  ],
  "extends": [
    "tslint-config-standard",
    "tslint-config-prettier"
  ],
  "rules": {
    "prettier": [
      true,
      {
        "singleQuote": true,　// 文字列はシングルクオートで囲む
        "semi": false         // 文末のセミコロン(;)は省く。prettierのデフォルトはtrue
      }
    ]
  }
}
```

## webpackの設定
まずは以下のコマンドでファイルを作成します。

```sh
$ touch webpack.config.js
```

```js:webpack.config.js
module.exports = {
  mode: process.env.NODE_ENV || "development",
  entry: "./src/index.ts",
  output: {
    filename: "bundle.js",
    path: __dirname + "/dist"
  },
  resolve: {
    extensions: [".ts", ".tsx", ".js", ".json"]
  },
  module: {
    rules: [
      {
        test: /\.ts?$/,
        use: [
          // 下から順に処理される
          { loader: "babel-loader" },
          { loader: "ts-loader" }
        ],
        exclude: /node_modules/
      }
    ]
  },
};
```

今回は`src/index.ts`をビルドし、`dist/bundle.js`として出力する前提で記載しています。
複数ファイルの場合は配列やhashでの設定も可能です。
注意する点としてはloaderは下から上の順に実行されるので、

1.ts-loaderでtypescript -> ES6へ変換
2.babel-loaderでES6 -> ES5へ変換

というようになっています。

## ビルド用のtypescriptファイル準備
試しにビルドする用のtypescriptを用意しましょう。
今回は`src/index.ts`を用意し、`dist/bundle.js`として出力したいので準備をします。

```sh
$ mkdir src dist
$ touch src/index.ts
```

中身は適当なtypescriptにしましょう。

```typescript:index.ts
const hoge: string = 'test'
console.log(hoge)
```

## package.jsonへscript設定
あとはこれらを実行するのですが、package.jsonのscript項目に記載しておけば、
npm(yarn) run [タスク名] で実行可能です。

```json:package.json
{
  "name": "hello_typescript",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "typescript": "^3.1.1",
    "webpack": "^4.20.2",
    "webpack-dev-server": "^3.1.9"
  },
  "scripts": {
    "build": "webpack",
    "watch": "webpack-dev-server",
    "check": "yarn run tsc --noEmit && yarn run tslint",
    "tslint": "tslint --project tsconfig.json",
    "tslint-fix": "tslint --fix --project tsconfig.json"
  },
  "devDependencies": {
    "@babel/core": "^7.1.2",
    "@babel/preset-env": "^7.1.0",
    "babel-loader": "^8.0.4",
    "prettier": "^1.14.3",
    "ts-loader": "^5.2.1",
    "tslint": "^5.11.0",
    "tslint-config-prettier": "^1.15.0",
    "tslint-config-standard": "^8.0.1",
    "tslint-plugin-prettier": "^2.0.0",
    "webpack-cli": "^3.1.2"
  }
}
```

## 実行
ようやくビルドできるようになりました。
実行してみましょう。

### 普通にビルド
```sh
$ yarn run build
yarn run v1.5.1
$ webpack
Hash: 8732ceeed4c190294712
Version: webpack 4.20.2
Time: 7067ms
Built at: 2018-10-05 22:46:29
    Asset      Size  Chunks             Chunk Names
bundle.js  3.84 KiB    main  [emitted]  main
Entrypoint main = bundle.js
[./src/index.ts] 52 bytes {main} [built]
✨  Done in 8.76s.
```
できました！
dist/bundle.jsが出来上がっていると思います。

### lintは動くか
では、src/index.tsを以下のように書き換えて、linterを動かすととどうなるか・・

```typescript:index.ts
const hoge: string="test";
console.log(hoge)
```

```sh
$ yarn run check
$ yarn run tsc --noEmit && yarn run tslint
$ /Users/yosuke/dev/src/github.com/tmzkysk/hello_typescript/node_modules/.bin/tsc --noEmit
$ tslint --project tsconfig.json

ERROR: /Users/tmzkysk/dev/src/github.com/tmzkysk/hello_typescript/src/index.ts[1, 19]: Replace `="test";` with `·=·'test'`
ERROR: /Users/tmzkysk/dev/src/github.com/tmzkysk/hello_typescript/src/index.ts[2, 18]: Insert `⏎`

error Command failed with exit code 2.
```

ちゃんとエラーになりました。
こういうときは`yarn run tslint-fix`とすれば直ります。

```
$ yarn run tslint-fix
yarn run v1.5.1
$ tslint --fix --project tsconfig.json
Fixed 1 error(s) in /Users/tmzkysk/hello_typescript/src/index.ts
```

中身を見るとしっかり直ってます。

```
$ cat src/index.ts
const hoge: string = 'test'
console.log(hoge)
```

### 型チェックとかはどうか
次にtypescriptのコンパイラ側のチェックがちゃんと動くか確認します。
index.tsを以下のようにしてビルドしてみます。

```typescript:index.ts
const hoge: number ="test"
console.log(hoge)
```

```sh
$ yarn run build
yarn run v1.5.1
$ webpack
Hash: 8732ceeed4c190294712
Version: webpack 4.20.2
Time: 3322ms
Built at: 2018-10-05 22:58:50
    Asset      Size  Chunks             Chunk Names
bundle.js  3.84 KiB    main  [emitted]  main
Entrypoint main = bundle.js
[./src/index.ts] 52 bytes {main} [built] [1 error]

ERROR in /Users/tmzkysk/hello_typescript/src/index.ts
./src/index.ts
[tsl] ERROR in /Users/hello_typescript/src/index.ts(1,7)
      TS2322: Type '"test"' is not assignable to type 'number'.
error An unexpected error occurred: "Command failed.
Exit code: 2
Command: sh
Arguments: -c webpack
Directory: /Users/tmzkysk/hello_typescript
```

number型にstring型を代入しているためしっかりエラーになりました。

### その他
package.jsonに設定した以下の内容について解説します。

```json:package.json
  "scripts": {
    "build": "webpack",
    "watch": "webpack-dev-server",
    "check": "yarn run tsc --noEmit && yarn run tslint",
    "tslint": "tslint --project tsconfig.json",
    "tslint-fix": "tslint --fix --project tsconfig.json"
  },
```

- build: webpack.config.jsの設定に沿ってビルドします。
- watch: webpack.config.jsの設定に沿ってビルドします。ファイルの変更を監視するのでtsファイル変更時に再度ビルドする必要がありません。
- check: typescript側のビルド時のチェックとTSLintのチェックをかけます。
- tslint: TSLintのチェックのみかけます。
- tslint-fix： TSLintのエラーを修正してくれます。

### webpack-dev-serverについて
基本的に開発時はファイル変更と同時にビルドが走って欲しいので、
webpack-dev-serverを使用することになります。
webpack-dev-serverはwebpack-cliと違い、
何も設定していなければ `localhost:8080` にビルド後のassetを吐き出すので、
railsでwebpackでビルドしたassetを参照する場合は`localhost:8080`から読み込むようにしてください。

## おわりに
`@babel/preset-typescript`がtsconfigの内容を踏襲してくれれば楽でしたが、
現状型チェックは提供されていないのでts-loaderをかませるようにしています。
今後のbabelに期待！