express-generatorで作成したexpressプロジェクトをTypeScript化する

はじめまして。katと申します。
本記事が初投稿になります。至らない点があるかもしれませんが、何卒ご容赦ください。

今回は、express-generatorで作成したexpressプロジェクトをTypeScript化する方法について、学んだ事項を紹介したいと思います。

工夫した点は以下の3点です。
- views, publicフォルダを使用できる
- 開発用実行として、ts-node-devを使ったホットリロードで実行する
- 本番用実行として、jsファイルに変換した上で実行する

Githubリポジトリ(今回までの作業)は[こちら](https://github.com/katkatprog/expressTs/tree/8f02d79538a39e0663764115da399f09f91fd1e1)です。

※なお、ブラウザ用Javascript (public/scriptsディレクトリに格納するJS) のTypeScript化は本記事では取り扱いません。

## 背景
つい最近、TypeScriptの基礎学習が終わり、何かしらアウトプットをしてみたいと思っておりました。
そこで、自室のラズパイ上で稼働中であるexpress-generatorで作成されたWEBアプリをTypeScript化してみることにしました。

その際、なるべくexpress-generatorの原型に沿ってTypeScript化したいと思ったため、views, publicフォルダに静的ファイルを格納して使用できるようにしたいと思いました。
また、実行方法に関しては、開発時はホットリロード(実行中にコードを書き換えた場合、サーバが自動でリロードされる方式)で実行でき、
その一方、本番実行の際はjsファイルに変換した上で実行できるのが望ましいと考えました。

そのような要件のもとで色々記事を探し、得られたノウハウをこの記事にまとめてみました。

## 前提
node.js, yarn, express-generatorは事前にインストールされていることを前提とします。

また、ツールの各バージョンは以下のとおりです。
- OS： macOS Big Sur バージョン11.6
- node.js: v16.14.0
- yarn: 1.22.17
- express-generator: 4.16.1

## expressプロジェクト作成
以下のコマンドを実行すると、sampleという名前のプロジェクトが作成されます。
```
express --git --view=ejs sample
```
--git オプションにより、プロジェクト内に.gitignoreファイルも一緒に作成されます。
--view=ejs オプションにより、テンプレートにejsが指定されます。

なお、express-generatorがグローバルインストールされておらず、
カレントディレクトリにのみインストールされているという方は、先頭にnpxを付けてコマンド実行をしてください。
```
npx express --git --view=ejs sample
```

## expressプロジェクト 動作確認
sampleディレクトリに移動し、動作確認します。
```
cd sample
yarn install 
yarn start
```

localhost:3000 にアクセスし以下の画面が表示されれば、expressプロジェクト作成成功です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2551098/cf921b8b-6935-cb47-8ea8-0d3680fadc09.png)

## パッケージのインストール
まず、プロジェクト配下にtypescriptをインストールします。
(typescriptをグローバルインストールしている方も含め。これが無いとts-nodeやts−node-devが動作しない。)
```
yarn add typescript
```

開発用実行のための、ts-node-devをインストールします。
```
yarn add ts-node-dev
```

さらに、expressで使用するパッケージに対応するtypesパッケージをインストールします。(これが無いと型推論されない。)
```
yarn add -D @types/cookie-parser @types/debug @types/express @types/http-errors @types/morgan @types/node
```

## tsconfigの作成
以下コマンドを実行し、tsconfig.jsonを作成します。
```
npx tsc --init
```
下記のように、rootDir, outDirの値を変更します。
```json:tsconfig.json
{
  "compilerOptions": {
    ~~省略~~
    "rootDir": "./src",                                  /* Specify the root folder within your source files. */
    ~~省略~~
    "outDir": "./dist",                                   /* Specify an output folder for all emitted files. */
    ~~省略~~
  }
}
```

上記で指定したsrcディレクトリにtsファイルを格納するようにします。
また、tsファイルから変換されたjsファイルはdistディレクトリに格納されるようになります。

## srcディレクトリの作成 ＆ ファイルの移動 ＆ 拡張子の変更
srcディレクトリを作成し、そこにjsファイルを移動します。
```
mkdir src
mv app.js bin routes src
```
次に、jsファイルの拡張子を.tsに変更します。
app.js → app.ts
routes/index.js → routes/index.ts
routes/user.js → routes/user.ts
bin/www → bin/www.ts

最終的に、このようなディレクトリ構成、ファイル名になっていれば大丈夫です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2551098/592ca968-44f5-fe0e-f02b-b2517a7f475e.png)


## JavaScriptファイルのTypeScript化
内容を以下のように変更します。
### app.jsのTypeScript化
Githubにおける差分は[こちら](https://github.com/katkatprog/expressTs/commit/3a6a858555d1b66f6dee23e5f7bfe149734601c5)
```typescript:app.ts
import createError from "http-errors";
import express, { Request, Response, NextFunction } from "express";
import path from "path";
import cookieParser from "cookie-parser";
import logger from "morgan";

import { router as indexRouter } from "./routes/index";
import { router as usersRouter } from "./routes/users";

const app = express();

// view engine setup
app.set("views", path.join("views"));//__dirNameと書いてある箇所を除く！
app.set("view engine", "ejs");

app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join("public")));//__dirNameと書いてある箇所を除く！
app.use(express.static("public"));

app.use("/", indexRouter);
app.use("/users", usersRouter);

// catch 404 and forward to error handler
app.use(function (req, res, next) {
  next(createError(404));
});

// ↓ 下のapp.use((err, req, res, next)=>{...})の引数errに付けるオブジェクトの型。
// errに付ける型として、vscode内のErrorというオブジェクト型があるが、statusというプロパティが無いため、拡張したinterfaceを定義している。
interface ErrorWithStatus extends Error {
  status: number;
}

// error handler
// ↓に関しては、このままだと全引数ともに暗黙的Anyとなってしまう。
// req,res,nextに関しては@types/expressの型を使えるが、errに関しては型拡張の必要あり！(上で定義したinterfaceを用いる。)
app.use(function (
  err: ErrorWithStatus,
  req: Request,
  res: Response,
  next: NextFunction
) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get("env") === "development" ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render("error");
});

export default app;
```
### index.js, user.jsのTypeScript化
Githubにおける差分は[こちら](https://github.com/katkatprog/expressTs/commit/8faad6b8e0499106bfd5e35749ce48b54255db30)
```typescript:index.ts
import express from "express";
const router = express.Router();

/* GET home page. */
router.get("/", function (req, res, next) {
  res.render("index", { title: "Express" });
});

export { router };
```
```typescript:user.ts
import express from "express";
const router = express.Router();

/* GET users listing. */
router.get("/", function (req, res, next) {
  res.send("respond with a resource");
});

export { router };
```
### wwwのTypeScript化
Githubにおける差分は[こちら](https://github.com/katkatprog/expressTs/commit/f806c049b72f6da9458c655f238780df87394bee)
```typescript:www.ts
#!/usr/bin/env node

/**
 * Module dependencies.
 */

import app from "../app";
import * as http from "http";
import * as debugModule from "debug";
var debug = debugModule.debug("quick-start-express-typescript:server");

/**
 * Get port from environment and store in Express.
 */

const port = normalizePort(process.env.PORT || "3000");
app.set("port", port);

/**
 * Create HTTP server.
 */

const server = http.createServer(app);

/**
 * Listen on provided port, on all network interfaces.
 */

server.listen(port);
server.on("error", onError);
server.on("listening", onListening);

/**
 * Normalize a port into a number, string, or false.
 */

function normalizePort(val: string): number | string | boolean {
  const nport = parseInt(val, 10);

  if (isNaN(nport)) {
    // named pipe
    return val;
  }

  if (nport >= 0) {
    // port number
    return nport;
  }

  return false;
}

/**
 * Event listener for HTTP server "error" event.
 */

function onError(error: any): void {
  if (error.syscall !== "listen") {
    throw error;
  }

  const bind = typeof port === "string" ? "Pipe " + port : "Port " + port;

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case "EACCES":
      console.error(bind + " requires elevated privileges");
      process.exit(1);
    case "EADDRINUSE":
      console.error(bind + " is already in use");
      process.exit(1);
    default:
      throw error;
  }
}

/**
 * Event listener for HTTP server "listening" event.
 */

function onListening(): void {
  function bind() {
    const addr = server.address();
    if (addr === null) {
      return "";
    }

    if (typeof addr === "string") {
      return "pipe " + addr;
    }

    if ("port" in addr) {
      return "port " + addr.port;
    }
  }

  debug("Listening on " + bind());
}
```

## 開発用実行
scripts内にdevコマンドを加えます。
```json:package.json
{
  ~~省略~~
  "scripts": {
    "dev": "ts-node-dev ./src/bin/www",
  ~~省略~~
  },
  ~~省略~~
}

```
以下のコマンドでdevコマンド(開発用実行)ができます。
```
yarn run dev
```
localhost:3000にアクセスすると以下の画面が表示されるはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2551098/cf921b8b-6935-cb47-8ea8-0d3680fadc09.png)
ts-node-devで実行しているため、jsファイルへの変換がなく、さらにコードを編集した際にはホットリロードされます。

## 本番用実行
scripts内のstartコマンドを以下のように編集します。
```json:package.json
{
  ~~省略~~
  "scripts": {
    "start": "tsc && node ./dist/bin/www"
  ~~省略~~
  },
  ~~省略~~
}

```
以下のコマンドでstartコマンド(本番用実行)ができます。
```
yarn start
```
localhost:3000にアクセスすると以下の画面が表示されるはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2551098/cf921b8b-6935-cb47-8ea8-0d3680fadc09.png)
ここでは、tsファイルをjsに変換しdistディレクトリに格納後、格納されたjsファイルが実行されます。

以上です。

## 最後に
ご覧いただきありがとうございました。
今後も、TypeScript,Express,React,Next.jsについて学んだことを記事を上げていく予定です。

## 参考記事
[express-generatorで作成したプロジェクトでTypeScriptを使用する](https://qiita.com/shintaro_secual/items/05b1fe0b0109b6ed2002)
[DockerでTypeScript×Node.js×Expressの環境構築](https://qiita.com/tanakaPH/items/84aedaad8c0f5958a5f0)
[TypeScriptのExpressアプリケーションを作る](https://qiita.com/jumperson/items/e546137f6305ea98a673)