0
Help us understand the problem. What are the problem?

posted at

updated at

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

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

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

工夫した点は以下の3点です。

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

Githubリポジトリ(今回までの作業)はこちらです。

背景

つい最近、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

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

まず、プロジェクト配下に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の値を変更します。

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

JavaScriptファイルのTypeScript化

内容を以下のように変更します。

app.jsのTypeScript化

Githubにおける差分はこちら

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における差分はこちら

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 };
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における差分はこちら

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コマンドを加えます。

package.json
{
  ~~省略~~
  "scripts": {
    "dev": "ts-node-dev ./src/bin/www",
  ~~省略~~
  },
  ~~省略~~
}

以下のコマンドでdevコマンド(開発用実行)ができます。

yarn run dev

localhost:3000にアクセスすると以下の画面が表示されるはずです。
image.png
ts-node-devで実行しているため、jsファイルへの変換がなく、さらにコードを編集した際にはホットリロードされます。

本番用実行

scripts内のstartコマンドを以下のように編集します。

package.json
{
  ~~省略~~
  "scripts": {
    "start": "tsc && node ./dist/bin/www"
  ~~省略~~
  },
  ~~省略~~
}

以下のコマンドでstartコマンド(本番用実行)ができます。

yarn start

localhost:3000にアクセスすると以下の画面が表示されるはずです。
image.png
ここでは、tsファイルをjsに変換しdistディレクトリに格納後、格納されたjsファイルが実行されます。

以上です。

最後に

ご覧いただきありがとうございました。

本当はpublic/scriptsディレクトリに格納するブラウザ用JavascriptもTypeScriptで書けるようにし、
tsc&webpackでコンパイルしたものをscriptsディレクトリに格納されるところまでできれば良いのですが、
そうするとexpress-generatorの原型から離れてしまうと感じ、今回は保留とさせていただきました。

今後も、TypeScript,Express,React,Next.jsについて学んだことを記事を上げていく予定です。
また、いずれExpressの上位互換フレームワークであるNest.jsについても勉強し、記事を出してみたいと思うので
今後も温かい目で見守っていただけますと幸いです。

参考記事

express-generatorで作成したプロジェクトでTypeScriptを使用する
DockerでTypeScript×Node.js×Expressの環境構築
TypeScriptのExpressアプリケーションを作る

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
0
Help us understand the problem. What are the problem?