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にします。
$ npm i --save-dev typescript
###tsconfig.jsonを生成
tsconfig.jsonはコンパイルオプションを詰め込んだファイルです。
以下のコマンドで生成します。
$ npx tsc --init
tsconfig.jsonをチューニング
デフォルトの設定から変更していきます。
target
target
は変換後のECMA Versionを指定できるようです。
導入するサービスはnode12なので、ES2019
にしました。(参考: https://node.green/)
"target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
allowJs
allowJs
を有効にすることで、既存のJSもコンパイル対象に含めます。
既存のJSファイルもそのまま動かしたいので、有効にします。
"allowJs": true, /* Allow javascript files to be compiled. */
outDir
outDir
で出力先フォルダを指定します。 dist
に設定します。
"outDir": "./dist", /* Redirect output structure to the directory. */
strict
strictを無効にしました。これがデフォルトの有効だと以下の設定がまとめて有効になりますが、 alwaysStrict
これが罠でした。。。
// "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は無効にしました
bin/wwwの編集
変換先のapp.jsを使用したいので以下のように変更します。
- var app = require('../app');
+ var app = require('../dist/app');
package.jsonにビルドするスクリプトを追加
ビルドするスクリプトと、(開発しやすいように)watchとcleanを追加します。
"scripts": {
"build": "tsc",
"build:watch": "tsc --watch",
"clean": "rm -Rf dist",
}
これで $ npm run build
でビルドできるようになりました
実際にTypeScriptで開発したときのTIPS
###Expressの型定義を導入する
expressモジュールには型定義が含まれていないので、型定義を別途インストールします。
$ npm i --save-dev @types/express
こんな感じで書けるようになります。
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
を継承する型を作成し、それを使うようにしました。
import Express from 'express';
export interface OriginalRequest extends Express.Request {
user : {
id: number,
name: string,
}
}
以下のように使用します。
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を導入すればTypeScriptが書けるようです。また、jestの型 @types/jest
もインストールします。
$ npm i --save-dev ts-jest @types/jest
jest.config.jsでts-jestを有効にする
presetが用意されているので、特別な設定が必要なければこれを使用するのが楽だと思います。
tsは変換して、jsはそのままテストする 'ts-jest/presets/js-with-babel'
を有効にしました。
変換時には同じ階層のtsconfigを自動的に使用してくれるようです。
module.exports = {
preset: 'ts-jest/presets/js-with-babel',
}
CircleCIで実施するときハマったところ
CIでPUSH時の自動テストを実行しているのですが、ts-jestを導入したら何故かテストができないという現象が起こりました。(ローカルでは大丈夫だったのに)
実施ログは以下が出ており、一つもテストできていませんでした。
Too long with no output (exceeded 10m0s)
色々と調べたところ、JestをCIで実施するときには --runInBand
を設定することが推奨されているという情報を発見しました。
**メモ:**Jest テストの実行時には、
--runInBand
フラグを使用してください。 このフラグがない場合、Jest は、ジョブを実行している仮想マシン全体に CPU リソースを割り当てようとします。--runInBand
を使用すると、Jest は、仮想マシン内の仮想化されたビルド環境のみを使用するようになります。
--runInBand
はJestをシリアルで実行するオプションらしく、これを設定したら成功しました!
おそらく、ts-jest対応で負荷が増大し、CIではシリアルでないとテストが実行できなかったのでしょう。
ESLintもTypeScriptに対応させる
今まで使っていたESLintをそのまま使えるようにします。
TSLintというのも存在するようですが、ESLintのプラグインに統合されていく流れのようです。
関連パッケージのインストール
$ npm i --save-dev @typescript-eslint/eslint-plugin @typescript-eslint/parser
eslintrcを編集
以下のようにoverridesを書き加えます。
no-unused-vars
はTypeScriptでは想定どおりに機能しなかったので、TypeScript専用の設定を書き加えました。
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では一緒に開発する仲間を募集中です。