TypeScript 速習チュートリアル

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 --inittsconfig.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.jsontype: "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-nodenodemon は開発用(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.json

4. 基本的な 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 ハンドラー用の型定義された RequestResponseNextFunction
  • ユーザーデータの形状を保証する 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 dev

7.3 本番用ビルド

npm run build
npm start

8. ソースマップによるデバッグ

tsconfig.jsonsourceMap を有効にすると、コンパイル後のコードをデバッグし、元の .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 を組み合わせて検討する。