TypeScript と Node.js
1. なぜ Node.js で TypeScript を使うのか?
TypeScript は Node.js 開発に静的型付け(Static Typing)をもたらし、より優れたツール(Tooling)、コード品質の向上、そして開発体験の強化を実現します。
主なメリットは以下の通りです:
- JavaScript コードの型安全性(Type safety)の確保
- オートコンプリート(Autocompletion)による優れた IDE サポート
- 開発中の早期エラー検知
- コードの保守性とドキュメント性の向上
- 容易なリファクタリング(Refactoring)
前提条件: 最新の Node.js LTS(v18+ 推奨)と npm をインストールしておいてください。node -v および npm -v で確認可能です。
2. TypeScript Node.js プロジェクトのセットアップ
このセクションでは、TypeScript 用に構成された新しい Node.js プロジェクトを作成する手順を説明します。
注意: 開発中は TypeScript(.ts)を記述し、本番環境で Node.js が実行できるように JavaScript(.js)にコンパイル(Compile)します。
2.1 新規プロジェクトの初期化
mkdir my-ts-node-app
cd my-ts-node-app
npm init -y
npm install typescript @types/node --save-dev
npx tsc --init実行内容の解説:
typescript:TypeScript コンパイラ(tsc)を追加します。@types/node:Node.js の型定義を提供します。npx tsc --init:tsconfig.json設定ファイルを作成します。
2.2 ソースフォルダの作成
ソースコードは src/ に、コンパイル後の出力は dist/ に保持するようにします。
mkdir src
# 後ほど src/server.ts, src/middleware/auth.ts などのファイルを追加します2.3 TypeScript の構成
生成された tsconfig.json を編集します。
例
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}オプションのハイライト:
- rootDir/outDir:ソース(src)とビルド出力(dist)を分離します。
- strict:最も安全な型チェックを有効にします。
- esModuleInterop:CommonJS と ES モジュール間の相互運用をスムーズにします。
- sourceMap:コンパイル後のコードをデバッグするためのマップファイルを生成します。
CommonJS vs ESM: このガイドでは module: "commonjs" を使用しています。
ESM を使用する場合(package.json で type: "module" を指定)は、module: "nodenext" または node16 を設定し、一貫して import/export を使用してください。
2.4 ランタイムと開発用依存関係のインストール
HTTP ハンドリング用の Express と、便利な開発ツールをインストールします。
npm install express body-parser
npm install --save-dev ts-node nodemon @types/express 警告:ts-node と nodemon は開発用(Development)としてのみ使用してください。本番環境(Production)では、tsc でコンパイルし、出力された JS ファイルを Node で実行します。
3. プロジェクト構造
プロジェクトを整理された状態に保ちましょう:
my-ts-node-app/
src/
server.ts # サーバーのエントリーポイント
middleware/ # ミドルウェア
auth.ts
entity/ # データベースエンティティ
User.ts
config/ # 設定ファイル
database.ts
dist/ # コンパイル後のJS出力
node_modules/
package.json
tsconfig.json4. 基本的な TypeScript サーバーの例
この例では、TypeScript で書かれた最小限の Express サーバーを示します。型定義された User モデルといくつかのルート(Routes)が含まれています。
src/server.ts
import express, { Request, Response, NextFunction } from 'express';
import { json } from 'body-parser';
// ユーザーの型を定義
interface User {
id: number;
username: string;
email: string;
}
// Express アプリの初期化
const app = express();
const PORT = process.env.PORT || 3000;
// ミドルウェア
app.use(json());
// インメモリデータベース(シミュレーション)
const users: User[] = [
{ id: 1, username: 'user1', email: '[email protected]' },
{ id: 2, username: 'user2', email: '[email protected]' }
];
// ルート定義
app.get('/api/users', (req: Request, res: Response) => {
res.json(users);
});
app.get('/api/users/:id', (req: Request, res: Response) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) return res.status(404).json({ message: 'ユーザーが見つかりません' });
res.json(user);
});
app.post('/api/users', (req: Request, res: Response) => {
const { username, email } = req.body;
if (!username || !email) {
return res.status(400).json({ message: 'ユーザー名とメールアドレスは必須です' });
}
const newUser: User = {
id: users.length + 1,
username,
email
};
users.push(newUser);
res.status(201).json(newUser);
});
// エラーハンドリングミドルウェア
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(err.stack);
res.status(500).json({ message: '何らかのエラーが発生しました!' });
});
// サーバー起動
app.listen(PORT, () => {
console.log(`サーバーが http://localhost:${PORT} で起動しました`);
});TypeScript がここに追加したもの:
- Express ハンドラー用の型定義された
Request、Response、NextFunction。 - ユーザーデータの形状を保証する
Userインターフェース。 - 型定義されたルートパラメータやボディによる、より安全なリファクタリングと補完。
5. Express ミドルウェアでの TypeScript 活用
ミドルウェア(Middleware)も強力に型付けできます。
また、宣言の結合(Declaration Merging)を介して Express の型を拡張し、リクエストオブジェクトに認証済みユーザーデータを保存することも可能です。
src/middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
// Express の Request 型を拡張してカスタムプロパティを追加
declare global {
namespace Express {
interface Request {
user?: { id: number; role: string };
}
}
}
export const authenticate = (req: Request, res: Response, next: NextFunction) => {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ message: 'トークンが提供されていません' });
}
try {
// 実際のアプリでは、ここで JWT トークンを検証します
const decoded = { id: 1, role: 'admin' }; // デコードされたトークンのモック
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ message: '無効なトークンです' });
}
};
export const authorize = (roles: string[]) => {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ message: '認証されていません' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ message: '権限がありません' });
}
next();
};
};ルートでのミドルウェア使用例
// src/server.ts
import { authenticate, authorize } from './middleware/auth';
app.get('/api/admin', authenticate, authorize(['admin']), (req, res) => {
res.json({ message: `こんにちは 管理者 ID:${req.user?.id}` });
});6. データベースとの連携 (TypeORM の例)
TypeORM のような ORM を TypeScript のデコレータ(Decorators)とともに使用して、クラスをテーブルにマッピングできます。
開始前の準備:
- パッケージをインストール:
npm install typeorm reflect-metadata pg(PostgreSQL の場合)。 - デコレータを使用するため tsconfig.json で有効化:
{ "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true } } - アプリ起動時に一度
reflect-metadataをインポートします。
src/entity/User.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
username: string;
@Column({ unique: true })
email: string;
@Column({ select: false })
password: string;
@Column({ default: 'user' })
role: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}src/config/database.ts
import 'reflect-metadata';
import { DataSource } from 'typeorm';
import { User } from '../entity/User';
export const AppDataSource = new DataSource({
type: 'postgres',
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432'),
username: process.env.DB_USERNAME || 'postgres',
password: process.env.DB_PASSWORD || 'postgres',
database: process.env.DB_NAME || 'mydb',
synchronize: process.env.NODE_ENV !== 'production',
logging: false,
entities: [User],
migrations: [],
subscribers: [],
});サーバー起動前にデータソースを初期化
// src/server.ts
import { AppDataSource } from './config/database';
AppDataSource.initialize()
.then(() => {
app.listen(PORT, () => console.log(`サーバーが http://localhost:${PORT} で起動しました`));
})
.catch((err) => {
console.error('DB初期化エラー', err);
process.exit(1);
});7. 開発ワークフロー
7.1 package.json へのスクリプト追加
{
"scripts": {
"build": "tsc",
"start": "node dist/server.js",
"dev": "nodemon --exec ts-node src/server.ts",
"watch": "tsc -w",
"test": "jest --config jest.config.js"
}
} 注記:test スクリプトは任意であり、Jest がセットアップされていることを前提としています。
7.2 開発モードでの実行
npm run dev7.3 本番用ビルド
npm run build
npm start8. ソースマップによるデバッグ
tsconfig.json で sourceMap を有効にすると、コンパイル後のコードをデバッグし、元の .ts ファイルにマップし直すことができます。
node --enable-source-maps dist/server.jsヒント: VS Code を含むほとんどの IDE は、ソースマップが有効な場合にブレークポイントを使用した TypeScript デバッグをサポートしています。
9. ベストプラクティス
- 関数の引数と戻り値には常に型を定義する。
- オブジェクトの形状にはインターフェース(Interface)を使用する。
tsconfig.jsonで厳格モード(strict mode)を有効にする。- 実行時の型チェックには型ガード(Type guards)を使用する。
- TypeScript のユーティリティ型(Partial, Pick, Omit など)を活用する。
- 型定義は
.d.tsファイルにまとめる。 - 固定値のセットには Enum または const assertion を使用する。
- 複雑な型は JSDoc コメントでドキュメント化する。
- 機密情報や設定には環境変数を優先し、起動時に検証する。
ts-node/nodemonは開発時のみ使用し、本番環境ではコンパイルする。- 一貫したコード品質のために、ESLint + Prettier に
@typescript-eslintを組み合わせて検討する。