やること
ステップごとに分けてあるので、導入したい箇所だけつまみ食いすることも可能です。
気力が残っていれば、追記で本番環境の運用についても書きたいです…
- express.jsをDockerで起動
- TypeScript導入
- TSLint + Prettier導入
- node-ts導入: dev環境はトランスパイルなしでtsファイルのまま開発
- nodemon導入: dev環境はファイル変更を検知して自動リロード
記事ではnode.js 8.12を使用しています。
8系から、npm install時に自動的に--saveオプションが付くようになっていますので、
記事内で「あれ?--saveしてなくない?」とならないように一言補足。
まずは最小構成でexpress.js起動
準備
(追記)
エディタ補完を効かせるには結局MacにNode.js入れないといけなかったりしましたが、
VSCodeがDockerコンテナのリモート開発に対応したりして潮目も変わってきたので
過激派用の設定も追記しました。
Dockerで立ち上げた開発環境をVS Codeで開く!
A. MacにNode.js入れている場合
PJフォルダ(仮にts-express)を作成し、とりあえずMac環境でinit
npm init -y
B. MacにNode.js入れない硬派な場合
PJフォルダ(仮にts-express)を作成し、空のjsonを作成
(何はともあれpackage.jsonがないとDockerイメージのビルドに失敗するので)
echo "{}" >> package.json
Docker開発環境整備
Dockerfile 作成
dockerのなんやかんやは素晴らしい記事がたくさんあるので、簡潔な説明に留めます…
# ベースイメージはお好みで普通のでもalpineでも。
FROM node:8.12.0-slim
ENV APP_ROOT /app/
WORKDIR $APP_ROOT
# package.jsonとpackage-lock.jsonを先にコピー。
# package*.jsonだけを先に個別コピーすることで、パッケージ変更時は`RUN npm install`が走るが
# それ以外のファイル変更時は同コマンドにはキャッシュ利用で飛ばされるため、ビルド時間を短縮できる。
COPY package*.json $APP_ROOT
RUN npm install
# その他ファイルをコピー。
COPY . $APP_ROOT
docker-compose.yml 作成
dockerコマンド叩くときに毎回引数つけるのが面倒なので、docker-compose作ります。
version: '2.1'
services:
  web:
    build:
      context: .
      dockerfile: ./Dockerfile
    command: npm run dev
    environment:
      NODE_ENV: development
    ports:
      - '3000:3000'
    volumes:
      - .:/app
入る
docker-compose run --rm web bash
以降、別段コメントがなければbashコマンドはdocker内で実行します。
Mac側でnpm initしていない場合はここでやる
npm init
expressインストール
npm i express
テスト用src/index.js作成
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => res.send('Hello World!'));
app.listen(port, () => console.log(`Example app listening on port ${port}!`));
package.jsonにdev実行コマンドを追加
"scripts" {
 "dev": "node src/index.js"
}
起動テスト
# Macの別窓terminalで実行
docker-compose up --build web
# バックグラウンド実行したいときは-dオプション
# docker-compose up -d --build web
ブラウザでhttp://localhost:3000/を開くと Hello World! が出ます 
出たら一度 docker-compose down web で止めましょう。
-dオプション無しで起動してる場合はCtrl+cでも止められます。
(追記)
※docker-compose upは、2回目以降は実は毎回ビルドしなくても良かったりします。
docker-compose.ymlファイルのvolumesに書いてある通りファイルの変更は
dockerの中でいじろうがホスト側でいじろうが常に双方向に更新されます。
なので、キャッシュが効いて高速にビルドされるとはいえ、数秒の待ちが煩わしい人は
普段は--buildなし、何か挙動がおかしくなったら--build付けて再ビルドが
お手軽かなと思います。堅実に生きたい人は常に--buildで。
TypeScript導入
TypeScriptと型情報を追加
npm i --save-dev typescript @types/express
src/index.jsをsrc/index.tsに名前変更
とりあえずトランスパイル出来ることだけを確認したいので、中身は変更しない。
/distディレクトリを作成
mkdir dist
tsconfig.json を作成
requireの記法を使うなら "module": "commonjs"
importの記法を使うなら "module": "es6" (TSなら普通こっちですかね。)
{
    "compilerOptions": {
        "outDir": "./dist",
        "sourceMap": true,
        "module": "es6",
        "target": "es5",
        "moduleResolution": "node",
        "removeComments": true
    },
    "include": [
        "./src/**/*"
    ]
}
モジュールのインポートがcommonjsの場合はsrc/index.tsの1行目を書き換え
// const express = require('express'); // これを
import * as express from "express";    // こう
package.jsonのscriptsを編集
"scripts": {
  // distから走るように修正。
  "dev": "node dist/index.js",
  // 追加。TypeScriptをグローバルではなくローカルインストールしているため、npm run tscでコンパイルがちゃんと走るようにする
  "tsc": "tsc"
}
トランスパイルする
npm run tsc
これでdistにトランスパイル後のファイルが出来上がるので、改めて走らせてみる
# Macの別窓terminalで実行。すでに走っている場合はCtrl+Cで一度止める。
docker-compose up --build web
TSLint + Prettier導入(入れたい方のみ)
(2019/11/11追記)
こちら の記事にあるように、現在はTSLintからESLint TypeScript Pluginへの移行が推奨されています。
TSLint 開発チームは ESLint のプラグインとして TSLint の機能を統合していく typescript-eslint プロジェクトを開始し、TSLint は 2019年中に非推奨となる予定である ことが発表されました。
よって、長期的にメンテナンスが見込まれる開発の場合はLinter/Formatter導入について VSCodeでESLint+@typescript-eslint+Prettierを導入する(v2.0.0修正版) 等を参考にして下さい 
---------- 以下、既存のTSLint + Prettier導入記事 ----------
(ESLintの場合はこのブログ記事が参考になります。 ESLint(あるいはTSLint)とPrettierを併用する)
リンターとフォーマッターの導入です。
両方入れる方法はいくつかあるのですが、ここではTSLintにPrettierの処理もまかせる方式で行きます。
この方式だと、設定ファイルがtslint.json一箇所にまとまって見通しが良くなります。
npm i -D tslint prettier tslint-config-prettier tslint-plugin-prettier
〜簡潔に説明〜
- 
tslintリンター本体
- 
prettierフォーマッター本体
- 
tslint-config-prettierTSLintとPrettierの重複処理を無効化
- 
tslint-plugin-prettierESLintのルールとしてPrettierを読み込む
(optional)ベースルールを使用したい人は導入します。僕の場合はeslintのstandardが好きなので、
そのルールをTSに合うようにカスタマイズされたtslint-config-standardを使用します。
npm i -D tslint-config-standard
tslint.jsonファイルを作成し、設定を書きます。
extendsのtslint-config-standardは上記で導入したルールに読み替えてください。
入れなかった場合はtslint-config-prettierだけ記述すればOKです。
{
  "rulesDirectory": ["tslint-plugin-prettier"],
  "extends": ["tslint-config-standard", "tslint-config-prettier"],
  "rules": {
    // TSLintに任せる方法の場合、prettier設定もここに書くことになります。
    // 設定内容はstandardだとsingleQuoteとsemiだけ気をつければ多分大丈夫です。
    // また、完全に僕の好みですが行あたり80文字は短すぎるので長めにしてあります。
    "prettier": [
      true, {
        "singleQuote": true,
        "semi": false,
        "printWidth": 120 // defaultの80って短くないですか?
      }
    ]
  }
}
上記を動かせるように、lintコマンドを追加します。
"script" {
  "lint": "tslint --exclude **/*.d.ts --project . --fix 'src/**/*.ts' 'test/**/*.ts'"
}
--project .は付けないと、rule requires type informationの警告が大量に出るため付けています。
テストディレクトリを含めるかどうかはお好みで。
またlintコマンドのオプションは色々な指定方法があるので、PJに合わせて適宜変更してください。
では、作成したlintコマンドテストをテスト。
npm run lint
自動修正できないものは無理ですが、修正できるものはこれで一発修正されます。
例えば、standardルールなら、最初に作ったsrc/index.tsの;が全部吹っ飛んだはずです。
(optional)VSCodeの人
TSLintプラグインを導入し、設定で "tslint.autoFixOnSave": true にすれば
保存のたびにLint+Prettierしてくれます。
ts-nodeを使ってトランスパイルなしで開発
ここまでくればTSでコードを書いていけるようになりましたが、コードを変更するたびに
npm run tscしてdocker-compose up webし直さなければなりません。それは流石にキチィ。
というわけで、ts-nodeを入れます。こいつを入れることで、nodeコマンドでjsファイルを
実行するのと同じようにts-nodeでtsファイルを実行できるようになります。
インストール
npm i -D ts-node
設定
npm run devコマンドでnodeではなくts-nodeを使用し、srcのtsファイルを直接読むようにします。
また、本番環境ではトランスパイル後の生jsを走らせると思うのでdistからの実行コマンドを別で用意します。
ts-nodeは例によってローカルインストールのため、パスが通るように手動でscriptに追加します。
追記: ts-nodeの本番環境運用も試しており、結論から言うと起動時間が若干長いという以外の
オーバーヘッドは殆ど無いので、本番実行も ts-node ./src/index.ts でも良いと思います。
"scripts": {
  "start": "node dist/index.js",    // 本番実行用のコマンド。これの前にtscも走らせること。
  "dev": "ts-node ./src/index.ts",  // 開発環境用のコマンド。
  "ts-node": "ts-node"
}
テスト
# Macの別窓terminalで実行。すでに走っている場合はCtrl+Cで一度止める。
docker-compose up --build web
トランスパイルしなくても、直接tsファイルを読み出すようになりました。
変更検知して自動リロード
tsファイルを直読めるようになったとはいえ、まだこのままではファイルに変更が加わるたびにサーバーを止めて
'npm start'し直さなければなりません。それは流石にキチィ。
というわけで、nodemonを入れます。こいつを入れることでコードの変更を検知して
サーバーをオートリロード出来るようになります。
インストール
npm i -D nodemon
設定ファイルnodemon.jsonを新規作成
{
  "watch": ["src"],
  "ext": "ts",
  // テストファイル等、リロード対象にしたくない物があればここに追加
  "ignore": ["tests/**/*.ts"],
  // 実際に実行するコマンド。先程のts-nodeのコマンドです。
  "exec": "npm run ts-node ./src/index.ts"
}
devコマンドでnodemonが起動するように修正
"scripts": {
  "dev": "nodemon -L"
}
テスト
# Macの別窓terminalで実行。すでに走っている場合はCtrl+Cで一度止める。
docker-compose up --build web
試しにsrc/index.jsのconsole.logのコメントをちょっといじって保存してみて下さい。
docker-composeの実行画面にリロード状況が出て、リロード後ブラウザでlocalhost:3000を
開き直すと変更が適用されているのが分かると思います。
おわり
これで、快適なTypeScript + Express.js開発環境を手に入れることができました。
Dockerfile, docker-compose.yml, その他lint, prettier等の各種設定は
プロジェクトにより色々変わってくると思うので、この記事はあくまでベースとして使用して
修正加えてください。
以下もお供にどうぞ
コントローラー
モデル
