Node.js + Expressで稼働中のサービスにTypeScriptを導入する

# Node.js + Expressで稼働中のサービスにシームレスにTypeScriptを導入する

GlobalMobilityService株式会社でバックエンドエンジニアとして活動しているEkeMinusYouです。

弊社で開発しているプラットフォームでは、Node.jsを全面的に採用しているのですが、TypeScriptで開発できる環境を整えたので、手順やハマったことについてご紹介します。

## 概要

Node.js + Express で稼働しているシステムがあり、これをTypeScriptで書けるようにしました。

TypeScriptにしたかった理由としては、以下の点が挙げられます。

- 保守性が高い良質なコードを生み出していきたい
- チーム規模拡大に向けて、型を使えるようにしたかった
- ~~ただTypeScript書きたかった~~

なお、TypeScriptを導入するにあたり以下の環境も手を入れました。

- テストも TypeScriptで開発できるようにした
  - Jestを採用しているので、ts-jestを導入
- 今まで使用していたESLint環境もTypeScriptに対応させた

なるべく、ストレスなくTypeScriptで開発できる環境を用意するのが目標です。

## TypeScript導入

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

typescriptをインストールします。本番環境では不要なパッケージなので、devにします。

```bash
$ npm i --save-dev typescript
```

###tsconfig.jsonを生成

tsconfig.jsonはコンパイルオプションを詰め込んだファイルです。

以下のコマンドで生成します。

``` bash
$ npx tsc --init
```

### tsconfig.jsonをチューニング

デフォルトの設定から変更していきます。

#### target

 `target` は変換後のECMA Versionを指定できるようです。

導入するサービスはnode12なので、`ES2019` にしました。（参考:  https://node.green/）

```json
    "target": "ES2019",                       /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
```

#### allowJs

 `allowJs` を有効にすることで、既存のJSもコンパイル対象に含めます。

既存のJSファイルもそのまま動かしたいので、有効にします。

``` json
    "allowJs": true,                          /* Allow javascript files to be compiled. */
```

#### outDir

 `outDir` で出力先フォルダを指定します。 `dist`に設定します。

``` json
    "outDir": "./dist",                       /* Redirect output structure to the directory. */
```

#### strict

strictを無効にしました。これがデフォルトの有効だと以下の設定がまとめて有効になりますが、 `alwaysStrict` これが罠でした。。。

```json
    // "noImplicitAny": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */
    // "strictNullChecks": true,              /* Enable strict null checks. */
    // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
    // "strictBindCallApply": true,              /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
    // "strictPropertyInitialization": true,     /* Enable strict checking of property initialization in classes. */
    // "noImplicitThis": true,                   /* Raise error on 'this' expressions with an implied 'any' type. */
    // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */
```

alwaysStrict は有効にすると、生成されるJSファイルに必ず `'use strict'` が付与されるという便利オプションです。 しかし、予想外だったのが、`allowJs` が有効になっている場合は変換元のファイルがTSだけでなくJSの場合も対象になっていたことです。

そのため、`'use strict'` がもともと付いていなかったJSファイルまで、変換後に勝手に `'use strict'` 付与されてエラーが発生してしまいました…

このときコンパイル時にエラーが起きず、実行する段階でしか補足できないので、影響範囲が想定しにくいです。~~そもそも、そんなコードがあるのが悪いという話はある~~

なので、なくなくstrictは無効にしました:cry:

### bin/wwwの編集

変換先のapp.jsを使用したいので以下のように変更します。

``` diff
- var app = require('../app');
+ var app = require('../dist/app');
```

### package.jsonにビルドするスクリプトを追加

ビルドするスクリプトと、（開発しやすいように）watchとcleanを追加します。

``` json
  "scripts": {
    "build": "tsc",
    "build:watch": "tsc --watch",
    "clean": "rm -Rf dist",
  }
```

これで `$ npm run build` でビルドできるようになりました :clap:

## 実際にTypeScriptで開発したときのTIPS

###Expressの型定義を導入する

expressモジュールには型定義が含まれていないので、型定義を別途インストールします。

``` bash
$ npm i --save-dev @types/express
```

こんな感じで書けるようになります。

``` typescript
import Express from 'express';
const router = Express.Router();

router.use('/', async (req: Express.Request, res: Express.Response, next: Express.NextFunction) => {
	next();
});
```

### req.userの型を有効にする

認証時にreq.userにユーザー情報に関する幾つかの値を格納しているのですが、 `Express.Request` はそこまで考慮してくれません。（当然ですが）

そのため、 `Express.Request` を継承する型を作成し、それを使うようにしました。

``` typescript
import Express from 'express';

export interface OriginalRequest extends Express.Request {
	user : {
		id: number,
		name: string,
	}
}
```

以下のように使用します。

``` typescript
import Express from 'express';
import {OriginalRequest} from '../types/request';
const router = Express.Router();

router.get('/', async (req: OriginalRequest, res: Express.Response) => {
  const name = OriginalRequest.user.name;
	return req.send(name).end();
});
```

## テストもTypeScriptで書けるようにする

TypeScriptで開発できるようにしたなら、テストもTypeScriptで書きたいので導入しました。（shirubaさんが頑張ってくれた）

### 関連パッケージの導入

Jestを採用しているのですが、[ts-jest](https://kulshekhar.github.io/ts-jest/)を導入すればTypeScriptが書けるようです。また、jestの型 `@types/jest` もインストールします。

``` bash
$ npm i --save-dev ts-jest @types/jest
```

### jest.config.jsでts-jestを有効にする

[preset](https://kulshekhar.github.io/ts-jest/user/config/)が用意されているので、特別な設定が必要なければこれを使用するのが楽だと思います。

tsは変換して、jsはそのままテストする `'ts-jest/presets/js-with-babel'` を有効にしました。

変換時には同じ階層のtsconfigを自動的に使用してくれるようです。

``` javascript
module.exports = {
  preset: 'ts-jest/presets/js-with-babel',
}
```

### CircleCIで実施するときハマったところ

CIでPUSH時の自動テストを実行しているのですが、ts-jestを導入したら何故かテストができないという現象が起こりました。（ローカルでは大丈夫だったのに）

実施ログは以下が出ており、一つもテストできていませんでした。

``` bash
Too long with no output (exceeded 10m0s)
```

色々と調べたところ、JestをCIで実施するときには `--runInBand`を設定することが推奨されているという情報を発見しました。

[テストメタデータの収集](https://circleci.com/docs/ja/2.0/collect-test-data/#jest)

> **メモ：**Jest テストの実行時には、`--runInBand` フラグを使用してください。 このフラグがない場合、Jest は、ジョブを実行している仮想マシン全体に CPU リソースを割り当てようとします。 `--runInBand` を使用すると、Jest は、仮想マシン内の仮想化されたビルド環境のみを使用するようになります。

`--runInBand` はJestをシリアルで実行するオプションらしく、これを設定したら成功しました！

おそらく、ts-jest対応で負荷が増大し、CIではシリアルでないとテストが実行できなかったのでしょう。

## ESLintもTypeScriptに対応させる

今まで使っていたESLintをそのまま使えるようにします。

TSLintというのも存在するようですが、ESLintのプラグインに統合されていく流れのようです。

https://qiita.com/suzuki_sh/items/fe9b60c4f9e1dbc5d903

#### 関連パッケージのインストール

``` bash
$ npm i --save-dev @typescript-eslint/eslint-plugin @typescript-eslint/parser
```

#### eslintrcを編集

以下のようにoverridesを書き加えます。

`no-unused-vars` はTypeScriptでは想定どおりに機能しなかったので、TypeScript専用の設定を書き加えました。

``` javascript
module.exports = {
	overrides: [{
		files: '*.ts',
		plugins: ["@typescript-eslint"],
		parser: '@typescript-eslint/parser',
		rules: {
			"no-unused-vars": 'off',
			"@typescript-eslint/no-unused-vars": ["error", {"vars": "all", "args": "none"}]
		},
	}],
};
```

## おわりに

Node.js + Expressで開発しているサービスに、TypeScript環境を導入する手順と、関連する環境の整備についてご紹介しました。
TypeScript でガンガン開発していきましょう！

GMSでは一緒に開発する仲間を募集中です。

https://www.wantedly.com/companies/company_8204706