6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Global Mobility ServiceAdvent Calendar 2019

Day 15

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

Last updated at Posted at 2019-12-14

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は無効にしました:cry:

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 でビルドできるようになりました :clap:

実際に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では一緒に開発する仲間を募集中です。

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?