Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

This article is a Private article. Only a writer and users who know the URL can access it.
Please change open range to public in publish setting if you want to share this article with other users.

TypeScript入門3(ファイル分割)

Posted at

TypeScript でのファイル分割方法:保守性の高いコードを書くためのベストプラクティス

目次

  1. なぜファイルを分割するのか?
  2. TypeScript におけるモジュールとスクリプトの判定
  3. モジュールシステム一覧
  4. 基本的なファイル分割パターン
  5. モジュールシステムの活用
  6. ブラウザで ES モジュールを直接利用する方法
  7. 実践的なファイル構成例
  8. ベストプラクティス
  9. トラブルシューティング
  10. TypeScript と Webpack ガイド
  11. まとめ

TypeScript プロジェクトが成長するにつれて、コードを適切に分割し整理することは、保守性とスケーラビリティを向上させるために不可欠です。この記事では、効果的なファイル分割戦略とベストプラクティスについて詳しく解説します。

なぜファイルを分割するのか?

メリット

  • 保守性の向上: 小さなファイルは理解しやすく、変更しやすい
  • 再利用性: 機能ごとに分割することで、他の部分で再利用可能
  • チーム開発: 複数人での並行開発が容易
  • テストの簡素化: 単一責任の原則に従ったモジュールはテストしやすい
  • パフォーマンス: 必要な部分のみをインポートできる

TypeScript におけるモジュールとスクリプトの判定

TypeScript では、ファイルがモジュールとして扱われるかスクリプトとして扱われるかによって、動作が大きく異なります。この違いを理解することは、適切なファイル分割戦略を立てる上で重要です。

モジュールとスクリプトの判定基準

TypeScript コンパイラーは以下の条件でファイルの種類を判定します:

モジュールとして判定される条件(いずれか一つでも該当)

  1. トップレベルで import/export を使用
// これらがあると自動的にモジュールになる
import { something } from "./other";
export const value = 42;
export default class MyClass {}
  1. package.json で type: "module"を指定
// package.json
{
  "type": "module"
}
  1. ファイル拡張子が.mts
// math.mts(必ずモジュールとして扱われる)
const add = (a: number, b: number) => a + b;
export { add };

スクリプトとして判定される条件

上記の条件に該当しない場合、スクリプトとして扱われます:

// script.ts(import/exportがない)
const globalVariable = "これはグローバルスコープ";
function globalFunction() {
  console.log("グローバル関数");
}

実際の判定例

// ケース1: モジュール(exportがある)
// user.ts
interface User {
  id: number;
  name: string;
}
export { User }; // この行があるためモジュール

// ケース2: スクリプト(import/exportがない)
// config.ts
const API_URL = "https://api.example.com";
const TIMEOUT = 5000;
// グローバルスコープに定義される

// ケース3: モジュール(importがある)
// main.ts
import { User } from "./user"; // この行があるためモジュール
const currentUser: User = { id: 1, name: "Alice" };

// ケース4: 強制的にモジュール化
// utils.ts
const helper = () => "utility function";
export {}; // 空のexportでモジュール化

モジュールとスクリプトの違い

項目 モジュール スクリプト
スコープ ファイルローカル グローバル
変数の重複 他ファイルと独立 グローバルで競合
this(トップレベル) undefined グローバルオブジェクト
strict mode 自動的に適用 明示的に指定が必要
型の共有 export/import が必要 グローバルで自動共有

実践例での比較

スクリプトファイルの問題例

// types.ts(スクリプト)
interface User {
  id: number;
  name: string;
}

// main.ts(スクリプト)
const user: User = { id: 1, name: "Alice" }; // 型は自動で利用可能
var globalVar = "問題のある変数"; // グローバル汚染

// another.ts(スクリプト)
var globalVar = "同じ名前の変数"; // エラー:重複宣言

モジュールファイルの解決例

// types.ts(モジュール)
export interface User {
  id: number;
  name: string;
}

// main.ts(モジュール)
import { User } from "./types";
const user: User = { id: 1, name: "Alice" };
const localVar = "ローカルスコープ"; // 他ファイルに影響しない

// another.ts(モジュール)
const localVar = "同じ名前でもOK"; // 独立したスコープなので問題なし

TypeScript 設定による制御

tsconfig.json での設定

{
  "compilerOptions": {
    "module": "ES2020",
    "moduleDetection": "auto", // 自動判定(デフォルト)
    "isolatedModules": true, // 各ファイルを個別モジュールとして扱う
    "verbatimModuleSyntax": true // import/export構文を厳密にチェック
  }
}

moduleDetection オプション

{
  "compilerOptions": {
    "moduleDetection": "auto" // 自動判定(推奨)
    // "moduleDetection": "legacy", // 従来の判定方式
    // "moduleDetection": "force"   // 全てモジュールとして扱う
  }
}

ベストプラクティス

1. 明示的なモジュール化

// 推奨:明示的にモジュールとして宣言
// constants.ts
export const API_URL = "https://api.example.com";
export const TIMEOUT = 5000;

// または空のexportでモジュール化
// types.ts
interface Config {
  apiUrl: string;
  timeout: number;
}

// 明示的にモジュール化
export type { Config };
// または
export {};

2. スクリプトの適切な使用場面

// スクリプトが適している例:設定ファイル
// global.d.ts
declare global {
  interface Window {
    myApp: {
      version: string;
      config: AppConfig;
    };
  }
}

// このファイルはスクリプトとして動作し、グローバル型を拡張

3. 混在を避ける

// ❌ 避けるべき:同じプロジェクト内でモジュールとスクリプトが混在
// script1.ts(スクリプト)
const config = { api: "url" };

// module1.ts(モジュール)
import { config } from "./script1"; // エラー:スクリプトからはimportできない

// ✅ 推奨:統一してモジュールを使用
// config.ts(モジュール)
export const config = { api: "url" };

// main.ts(モジュール)
import { config } from "./config"; // 正常に動作

デバッグとトラブルシューティング

ファイルの種類を確認する方法

// TypeScriptコンパイラーでの確認
// tsc --listFiles でコンパイル対象ファイルを確認
// tsc --showConfig で設定を確認

// ファイル内での確認方法
console.log("Is module:", typeof exports !== "undefined"); // モジュールかどうか
console.log("Global this:", typeof globalThis); // グローバルオブジェクトの確認

よくあるエラーと解決法

// エラー1: "Cannot use import statement outside a module"
// 解決法:ファイルをモジュール化
export {}; // この行を追加

// エラー2: "Top-level 'await' expressions are only allowed when..."
// 解決法:モジュール化してESモジュールで実行
export {}; // モジュール化
const data = await fetch("/api/data"); // Top-level awaitが使用可能

// エラー3: 型の重複宣言
// 解決法:名前空間またはモジュールで分離
export namespace UserTypes {
  export interface User {
    id: number;
  }
}
export namespace AdminTypes {
  export interface User {
    id: string;
    permissions: string[];
  }
}

この理解により、TypeScript プロジェクトで適切なファイル分割戦略を立て、予期しない動作を避けることができます。

モジュールシステム一覧

TypeScript で使用可能な主要なモジュールシステムの比較表です:

モジュールシステム 構文例 特徴 使用場面 推奨度
ES モジュール import/export 静的解析、Tree Shaking、標準仕様 モダン Web 開発、フロントエンド ⭐⭐⭐⭐⭐
CommonJS require/module.exports Node.js 標準、動的読み込み レガシー Node.js、一部ライブラリ ⭐⭐⭐
AMD define/require 非同期読み込み、ブラウザ特化 レガシーブラウザアプリ
UMD 複数形式対応 環境に応じて自動切り替え ライブラリ作成 ⭐⭐
SystemJS System.register 動的モジュール読み込み 複雑なモジュール管理 ⭐⭐

各モジュールシステムの詳細

ES モジュール(ES2015+)

// エクスポート
export const func = () => {};
export default class MyClass {}

// インポート
import MyClass, { func } from "./module";

CommonJS(Node.js)

// エクスポート
exports.func = () => {};
module.exports = MyClass;

// インポート
const { func } = require("./module");
const MyClass = require("./module");

AMD(Asynchronous Module Definition)

// 定義
define(["dependency"], function (dep) {
  return { func: () => {} };
});

// 使用
require(["module"], function (module) {
  module.func();
});

UMD(Universal Module Definition)

(function (root, factory) {
  if (typeof define === "function" && define.amd) {
    define(["dependency"], factory); // AMD
  } else if (typeof module === "object" && module.exports) {
    module.exports = factory(require("dependency")); // CommonJS
  } else {
    root.MyModule = factory(root.Dependency); // グローバル
  }
})(typeof self !== "undefined" ? self : this, function (dependency) {
  return { func: () => {} };
});

基本的なファイル分割パターン

1. 機能別分割(Feature-based)

// types/user.ts
export interface User {
  id: number;
  name: string;
  email: string;
}

export interface CreateUserRequest {
  name: string;
  email: string;
}
// services/userService.ts
import { User, CreateUserRequest } from "../types/user";

export class UserService {
  async createUser(userData: CreateUserRequest): Promise<User> {
    // ユーザー作成ロジック
    return {
      id: Date.now(),
      ...userData,
    };
  }

  async getUser(id: number): Promise<User | null> {
    // ユーザー取得ロジック
    return null;
  }
}
// controllers/userController.ts
import { UserService } from "../services/userService";
import { CreateUserRequest } from "../types/user";

export class UserController {
  constructor(private userService: UserService) {}

  async handleCreateUser(request: CreateUserRequest) {
    try {
      const user = await this.userService.createUser(request);
      return { success: true, data: user };
    } catch (error) {
      return { success: false, error: error.message };
    }
  }
}

2. レイヤー別分割(Layer-based)

src/
├── models/          # データモデル
├── repositories/    # データアクセス層
├── services/        # ビジネスロジック層
├── controllers/     # プレゼンテーション層
├── types/           # 型定義
├── utils/           # ユーティリティ関数
└── config/          # 設定ファイル

モジュールシステムの活用

ES モジュール vs CommonJS

TypeScript でファイル分割を行う際、使用するモジュールシステムによってインポート・エクスポートの構文が異なります。現代的な TypeScript 開発では**ES モジュール(ES Modules)**の使用が推奨されます。

ES モジュール(推奨)

// ESモジュールの例
// math.ts
export const add = (a: number, b: number): number => a + b;
export default class Calculator {}

// main.ts
import Calculator, { add } from "./math";

CommonJS(従来型)

// CommonJSの例
// math.ts
const add = (a: number, b: number): number => a + b;
class Calculator {}

module.exports = { add, Calculator };

// main.ts
const { add, Calculator } = require("./math");

プロジェクト設定

ES モジュールを使用する場合(推奨設定)

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ES2020",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true
  }
}

// package.json
{
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node --loader ts-node/esm src/index.ts"
  }
}

CommonJS を使用する場合

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "moduleResolution": "node"
  }
}

// package.json("type": "module"の指定なし)
{
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

ES モジュールを推奨する理由

  1. Tree Shaking: 未使用のコードを自動的に除去し、バンドルサイズを最適化
  2. 静的解析: ビルド時に依存関係を解析でき、より高度な最適化が可能
  3. 標準化: JavaScript 標準仕様で、将来的な互換性が保証
  4. モダンツール: Vite、ESBuild、Rollup など最新ツールとの親和性が高い
  5. ブラウザネイティブサポート: 直接ブラウザで実行可能
  6. Top-level await: モジュールのトップレベルで await が使用可能
// ESモジュールでのTop-level awaitの例
// config.ts
const config = await fetch("/api/config").then((r) => r.json());
export default config;

ブラウザで ES モジュールを直接利用する方法

ES モジュールはブラウザでネイティブサポートされており、ローカルサーバーを用意することで直接利用できます。

ローカルサーバーの準備

方法 1: serve を使用(推奨)

# serveをグローバルインストール
npm install -g serve

# プロジェクトディレクトリで実行
serve . -p 3000

# またはnpxで一時的に使用
npx serve . -p 3000

# CORSを有効にしたい場合
serve . -p 3000 --cors

方法 2: Node.js の http-server を使用

# http-serverをグローバルインストール
npm install -g http-server

# プロジェクトディレクトリで実行
http-server . -p 3000 --cors

方法 3: Vite の開発サーバーを使用

# Viteをインストール
npm install -D vite

# package.jsonにスクリプト追加
# "scripts": { "dev": "vite --port 3000" }

# 実行
npm run dev

# またはnpxで直接実行
npx vite --port 3000

方法 4: webpack-dev-server を使用

# webpack-dev-serverをインストール
npm install -D webpack webpack-cli webpack-dev-server

# 実行
npx webpack serve --port 3000 --static .

# 設定ファイル使用時
npx webpack serve --config webpack.config.js

方法 5: Python の簡易サーバーを使用

# Python 3の場合
python -m http.server 3000

# Python 2の場合
python -m SimpleHTTPServer 3000

方法 6: PHP 内蔵サーバーを使用

# PHPがインストールされている場合
php -S localhost:3000 -t .

# 特定のルーターファイルを指定
php -S localhost:3000 -t . router.php

方法 7: Ruby 内蔵サーバーを使用

# Rubyがインストールされている場合
ruby -run -e httpd . -p 3000

# またはWEBrickを使用
ruby -r webrick -e "WEBrick::HTTPServer.new(Port: 3000, DocumentRoot: Dir.pwd).start"

方法 8: Deno を使用

# Denoがインストールされている場合
deno run --allow-net --allow-read https://deno.land/std@0.181.0/http/file_server.ts --port 3000

# または短縮形
deno run --allow-net --allow-read jsr:@std/http/file-server --port 3000

方法 9: Live Server を使用(VS Code 拡張)

VS Code の「Live Server」拡張機能を使用すると、右クリックから簡単にローカルサーバーを起動できます。

方法 10: Browsersync を使用

# Browsersyncをインストール
npm install -g browser-sync

# 実行(自動リロード付き)
browser-sync start --server --port 3000 --files "**/*"

# 特定のディレクトリを指定
browser-sync start --server --port 3000 --files "dist/**/*" --directory

方法 11: Express.js で簡単なサーバーを作成

// server.js
const express = require("express");
const path = require("path");
const app = express();

app.use(express.static("."));
app.use((req, res, next) => {
  res.header("Access-Control-Allow-Origin", "*");
  next();
});

app.listen(3000, () => {
  console.log("Server running on http://localhost:3000");
});
# 実行
node server.js

方法 12: Bun を使用

# Bunがインストールされている場合
bunx serve . --port 3000

# または自作のサーバー
bun run --hot server.ts

各サーバーの比較

サーバー インストール 特徴 推奨用途 自動リロード
serve npm i -g serve 軽量、高速、SPA 対応 モダン開発全般
http-server npm i -g http-server シンプル、CORS 設定豊富 基本的な静的サイト
Vite npm i -D vite 高速、HMR、TypeScript 対応 モダンフロントエンド
webpack-dev-server npm i -D webpack-dev-server 豊富な機能、バンドル対応 複雑な設定が必要な場合
Python 標準搭載 インストール不要 一時的な確認
PHP PHP 必要 PHP プロジェクトに最適 PHP 開発
Ruby Ruby 必要 Ruby プロジェクトに最適 Ruby 開発
Deno Deno 必要 セキュアなランタイム Deno 開発
Live Server VS Code 拡張 IDE 統合、自動リロード 開発中のプレビュー
Browsersync npm i -g browser-sync 自動リロード、同期 デザイン確認
Express npm i express カスタマイズ可能 カスタムサーバー 設定次第
Bun Bun 必要 高速ランタイム Bun 開発環境

用途別おすすめサーバー

  • 開発初心者: serve または Python http.server
  • TypeScript 開発: Vite
  • 自動リロードが欲しい: Browsersync または Live Server
  • 高度な設定が必要: webpack-dev-server
  • 一時的な確認: npx serve または Python http.server

プロジェクト構成例

project/
├── index.html
├── src/
│   ├── main.js          # TypeScriptコンパイル後
│   ├── utils/
│   │   ├── math.js
│   │   └── validation.js
│   └── services/
│       └── api.js
├── dist/                # TypeScriptビルド出力
└── tsconfig.json

TypeScript 設定

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ES2020",
    "moduleResolution": "node",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

HTML での ES モジュール読み込み

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>ESモジュール サンプル</title>
  </head>
  <body>
    <h1>ESモジュール デモ</h1>
    <button id="calculate">計算実行</button>
    <div id="result"></div>

    <!-- type="module" を指定してESモジュールとして読み込み -->
    <script type="module" src="./dist/main.js"></script>
  </body>
</html>

TypeScript モジュール例

// src/utils/math.ts
export const add = (a: number, b: number): number => {
  return a + b;
};

export const multiply = (a: number, b: number): number => {
  return a * b;
};

export default class Calculator {
  calculate(operation: string, a: number, b: number): number {
    switch (operation) {
      case "add":
        return add(a, b);
      case "multiply":
        return multiply(a, b);
      default:
        throw new Error("Unknown operation");
    }
  }
}
// src/services/api.ts
export interface ApiResponse<T> {
  data: T;
  status: number;
}

export class ApiClient {
  async fetchData<T>(url: string): Promise<ApiResponse<T>> {
    try {
      const response = await fetch(url);
      const data = await response.json();
      return { data, status: response.status };
    } catch (error) {
      throw new Error(`API Error: ${error}`);
    }
  }
}

export default ApiClient;
// src/main.ts
import Calculator, { add, multiply } from "./utils/math.js";
import ApiClient from "./services/api.js";

class App {
  private calculator: Calculator;
  private apiClient: ApiClient;

  constructor() {
    this.calculator = new Calculator();
    this.apiClient = new ApiClient();
    this.init();
  }

  private init(): void {
    const button = document.getElementById("calculate");
    const result = document.getElementById("result");

    button?.addEventListener("click", () => {
      // 計算デモ
      const sum = add(5, 3);
      const product = multiply(4, 7);
      const calcResult = this.calculator.calculate("add", 10, 15);

      if (result) {
        result.innerHTML = `
                    <p>5 + 3 = ${sum}</p>
                    <p>4 × 7 = ${product}</p>
                    <p>Calculator: 10 + 15 = ${calcResult}</p>
                `;
      }
    });
  }
}

// アプリケーション初期化
new App();

実行手順

  1. TypeScript を JavaScript にコンパイル
# TypeScriptコンパイラーでビルド
tsc

# またはwatchモードで自動コンパイル
tsc --watch
  1. ローカルサーバー起動
http-server . -p 3000 --cors
  1. ブラウザでアクセス
http://localhost:3000

注意点

  • CORS 制限: file://プロトコルでは ES モジュールは動作しないため、必ず HTTP サーバーが必要
  • 拡張子: インポート時には.js拡張子を明記(TypeScript ファイルでも)
  • モジュール対応ブラウザ: 比較的新しいブラウザ(IE 非対応)でのみ動作
  • 開発者ツール: ブラウザの開発者ツールでネットワークタブを確認し、モジュールの読み込み状況をチェック

デバッグのコツ

// デバッグ用のログ出力
console.log("Module loaded:", import.meta.url);

// 動的インポートも可能
async function loadModule() {
  const module = await import("./utils/helper.js");
  console.log("Dynamic import:", module);
}

この方法により、ビルドツールなしで ES モジュールの動作を直接確認でき、TypeScript の分割されたモジュール構造を理解しやすくなります。

Named Export vs Default Export

// utils/math.ts - Named Exportの例
export const add = (a: number, b: number): number => a + b;
export const multiply = (a: number, b: number): number => a * b;

// 使用例
import { add, multiply } from "./utils/math";
// services/apiClient.ts - Default Exportの例
class ApiClient {
  private baseUrl: string;

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  async get<T>(endpoint: string): Promise<T> {
    // GET リクエストの実装
    return {} as T;
  }
}

export default ApiClient;

// 使用例
import ApiClient from "./services/apiClient";

バレルエクスポート(Barrel Exports)

// types/index.ts
export * from "./user";
export * from "./product";
export * from "./order";
// 使用側での簡潔なインポート
import { User, Product, Order } from "./types";

実践的なファイル構成例

小規模プロジェクト

src/
├── types/
│   ├── index.ts
│   ├── user.ts
│   └── api.ts
├── services/
│   ├── index.ts
│   ├── userService.ts
│   └── apiService.ts
├── utils/
│   ├── index.ts
│   ├── validation.ts
│   └── formatting.ts
├── config/
│   └── constants.ts
└── index.ts

中規模〜大規模プロジェクト

src/
├── features/
│   ├── user/
│   │   ├── types/
│   │   ├── services/
│   │   ├── components/
│   │   └── index.ts
│   └── product/
│       ├── types/
│       ├── services/
│       ├── components/
│       └── index.ts
├── shared/
│   ├── types/
│   ├── utils/
│   ├── services/
│   └── constants/
├── core/
│   ├── api/
│   ├── auth/
│   └── config/
└── index.ts

ベストプラクティス

1. 単一責任の原則

各ファイルは一つの明確な責任を持つべきです。

// ❌ 悪い例: 複数の責任を持つファイル
// userUtils.ts
export const validateEmail = (email: string) => {
  /* ... */
};
export const formatUserName = (name: string) => {
  /* ... */
};
export const calculateAge = (birthDate: Date) => {
  /* ... */
};
export const sendWelcomeEmail = (user: User) => {
  /* ... */
};

// ✅ 良い例: 責任ごとに分割
// validation.ts
export const validateEmail = (email: string) => {
  /* ... */
};

// formatting.ts
export const formatUserName = (name: string) => {
  /* ... */
};

// calculations.ts
export const calculateAge = (birthDate: Date) => {
  /* ... */
};

// emailService.ts
export const sendWelcomeEmail = (user: User) => {
  /* ... */
};

2. 循環参照の回避

// ❌ 循環参照が発生する例
// userService.ts
import { OrderService } from "./orderService";

export class UserService {
  constructor(private orderService: OrderService) {}
}

// orderService.ts
import { UserService } from "./userService";

export class OrderService {
  constructor(private userService: UserService) {}
}
// ✅ 共通の型やインターフェースを分離
// types/index.ts
export interface IUserService {
  getUser(id: number): Promise<User>;
}

export interface IOrderService {
  getOrdersByUser(userId: number): Promise<Order[]>;
}

// userService.ts
import { IOrderService } from "../types";

export class UserService implements IUserService {
  constructor(private orderService: IOrderService) {}
}

3. 適切な命名規則

// ファイル名: kebab-case
user - service.ts;
api - client.ts;
validation - utils.ts;

// クラス名: PascalCase
export class UserService {}
export class ApiClient {}

// 関数・変数名: camelCase
export const validateEmail = () => {};
export const apiEndpoint = "https://api.example.com";

// 定数: SCREAMING_SNAKE_CASE
export const MAX_RETRY_COUNT = 3;
export const API_TIMEOUT = 5000;

4. インポートの整理

// インポートの順序
// 1. Node.jsの標準ライブラリ
import { readFile } from "fs/promises";

// 2. 外部ライブラリ
import axios from "axios";
import { z } from "zod";

// 3. 内部モジュール(絶対パス)
import { User } from "@/types/user";
import { ApiClient } from "@/services/apiClient";

// 4. 相対パス
import { validateInput } from "../utils/validation";
import { formatResponse } from "./helpers";

トラブルシューティング

よくある問題と解決策

問題 1: ES モジュールでのファイル拡張子エラー

// ❌ ESモジュールでは拡張子が必要な場合がある
import { utils } from "./utils";

// ✅ 明示的に拡張子を指定
import { utils } from "./utils.js"; // TypeScriptでも.jsを使用

問題 2: CommonJS ライブラリとの互換性

// ESModuleInteropを有効にして解決
// tsconfig.json
{
  "compilerOptions": {
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true
  }
}

// 使用例
import express from 'express'; // CommonJSライブラリでも自然にimport可能

問題 3: TypeScript の型解決エラー

// 解決策: tsconfig.jsonでパスマッピングを設定
{
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "@/*": ["*"],
      "@/types/*": ["types/*"],
      "@/services/*": ["services/*"]
    }
  }
}

問題 4: バンドルサイズの肥大化

// ❌ 全体をインポート
import * as utils from "./utils";

// ✅ 必要な部分のみインポート
import { validateEmail } from "./utils/validation";
import { formatDate } from "./utils/formatting";

まとめ

効果的なファイル分割は、TypeScript プロジェクトの成功に不可欠です。以下のポイントを心がけましょう:

  • 機能や責任に基づいて分割する
  • 循環参照を避ける
  • 一貫性のある命名規則を使用する
  • 適切なモジュールシステムを活用する
  • プロジェクトの規模に応じた構成を選択する

これらの原則に従うことで、保守性が高く、スケーラブルな TypeScript アプリケーションを構築できます。

TypeScript と Webpack ガイド:モダン Web アプリケーション開発の基礎

現代の Web アプリケーション開発において、TypeScript と Webpack の組み合わせは非常に強力で、多くの開発者に愛用されています。この記事では、TypeScript と Webpack の連携について、基本的な設定から実践的な運用まで詳しく解説します。

TypeScript と Webpack とは?

TypeScript

TypeScript は、Microsoft が開発した JavaScript の上位互換言語です。静的型付けによる型安全性、最新の ECMAScript 機能のサポート、優れた開発者体験を提供します。

Webpack

Webpack はモジュールバンドラーとして Web アプリケーション開発の標準的なツールです。複数のファイルやリソースを効率的に束ねて、最適化されたバンドルファイルを生成します。

なぜ Webpack を使うのか?

1. モジュールシステムの統一

従来の Web 開発では、複数の JavaScript ファイルを<script>タグで読み込む必要がありました。これにより以下の問題が発生していました:

<!-- 従来の方法 - 問題が多い -->
<script src="utils.js"></script>
<script src="api.js"></script>
<script src="components.js"></script>
<script src="app.js"></script>

問題点:

  • ファイルの読み込み順序に依存
  • グローバルスコープの汚染
  • 依存関係の管理が困難
  • HTTP リクエスト数の増加によるパフォーマンス低下

Webpack を使用することで、ES6 Modules や CommonJS などの様々なモジュールシステムを統一的に扱えます:

// モダンな方法 - Webpackが解決
import { fetchData } from "./api";
import { Button } from "./components/Button";
import { formatDate } from "./utils";

2. アセット管理の最適化

Webpack は単なる JavaScript バンドラーではありません。CSS、画像、フォントなどのあらゆるアセットを「モジュール」として扱います:

// JavaScript内でCSSを直接インポート
import "./styles.css";
import logoImage from "./assets/logo.png";

// TypeScriptでもアセットを型安全に扱える
const App: React.FC = () => (
  <div>
    <img src={logoImage} alt="Logo" />
  </div>
);

メリット:

  • 依存関係の明確化
  • 使用されていないアセットの自動除去
  • ファイルのハッシュ化による効率的なキャッシュ戦略
  • 画像の最適化や Base64 エンコードの自動化

3. 開発体験の向上

Hot Module Replacement (HMR)
コードを変更すると、ページを再読み込みせずに変更部分だけが即座に反映されます:

// 開発時の設定
module.exports = {
  devServer: {
    hot: true, // HMRを有効化
    open: true,
  },
};

Source Maps
本番環境では圧縮されたコードでも、デバッグ時には元の TypeScript コードでエラーを確認できます:

module.exports = {
  devtool: "source-map", // デバッグ情報を生成
};

4. パフォーマンス最適化

コード分割(Code Splitting)
大きなアプリケーションを小さなチャンクに分割し、必要な時だけ読み込むことができます:

// 動的インポートによる遅延読み込み
const LazyComponent = React.lazy(() => import("./LazyComponent"));

// Webpackが自動的に別ファイルとして分離

Tree Shaking
使用されていないコードを自動的に除去します:

// utils.js
export const usedFunction = () => console.log("used");
export const unusedFunction = () => console.log("unused"); // 自動で除去される

// app.js
import { usedFunction } from "./utils"; // unusedFunctionは最終バンドルに含まれない

圧縮と最適化

  • JavaScript の圧縮(Minification)
  • CSS の最適化
  • 画像の圧縮
  • 不要な空白やコメントの除去

5. 多様なファイル形式への対応

Webpack のローダーシステムにより、様々なファイル形式を JavaScript モジュールとして扱えます:

module.exports = {
  module: {
    rules: [
      { test: /\.tsx?$/, use: "ts-loader" }, // TypeScript
      { test: /\.css$/, use: ["style-loader", "css-loader"] }, // CSS
      { test: /\.scss$/, use: ["style-loader", "css-loader", "sass-loader"] }, // Sass
      { test: /\.(png|jpg|gif)$/, type: "asset/resource" }, // 画像
      { test: /\.svg$/, use: "@svgr/webpack" }, // SVG as React Component
    ],
  },
};

6. ビルドプロセスの自動化

環境別設定
開発環境と本番環境で異なる最適化を自動的に適用:

// 開発環境: 高速ビルド、デバッグ情報付き
// 本番環境: 圧縮、最適化、キャッシュ戦略

const isProduction = process.env.NODE_ENV === "production";

module.exports = {
  mode: isProduction ? "production" : "development",
  devtool: isProduction ? "source-map" : "eval-source-map",
};

プラグインエコシステム
豊富なプラグインにより、複雑なビルドタスクを自動化:

  • HTML ファイルの自動生成
  • CSS ファイルの分離
  • PWA 対応
  • バンドルサイズの解析
  • 古いファイルの自動削除

7. TypeScript との完璧な統合

型チェックの統合

// ts-loaderまたはesbuild-loaderでTypeScriptを処理
{
  test: /\.tsx?$/,
  use: {
    loader: 'ts-loader',
    options: {
      transpileOnly: true, // 高速化のため型チェックは別プロセス
    },
  },
}

パス解決の統合

// TypeScriptのpathsとWebpackのaliasを連携
// tsconfig.json
"paths": {
  "@/*": ["src/*"],
  "@components/*": ["src/components/*"]
}

// webpack.config.js
resolve: {
  alias: {
    '@': path.resolve(__dirname, 'src'),
    '@components': path.resolve(__dirname, 'src/components'),
  },
}

Webpack を使わない場合の課題

従来のアプローチの限界:

  • 手動でのファイル結合が必要
  • 依存関係管理の複雑さ
  • 本番環境での最適化が困難
  • 開発効率の低下
  • モジュールシステムの非対応ブラウザでの動作不良

他のツールとの比較:

  • Vite: 開発時は高速だが、複雑な設定が必要な場合は Webpack が有利
  • Rollup: ライブラリ開発には適しているが、アプリケーション開発では Webpack がより適している
  • Parcel: 設定不要だが、カスタマイズ性に制限がある

ts-loader を使った TypeScript の直接処理

Webpack ではts-loaderを使用することで、TypeScript ファイルを直接処理して JavaScript にトランスパイルできます。このセクションでは、ts-loader の詳細な設定方法と最適化手法について説明します。

ts-loader とは

ts-loaderは、Webpack で TypeScript ファイルを処理するための公式ローダーです。TypeScript コンパイラ(tsc)を Webpack のビルドプロセスに統合し、以下の機能を提供します:

  • TypeScript から JavaScript へのトランスパイル
  • 型チェックの実行
  • ソースマップの生成
  • インクリメンタルコンパイル

基本的なセットアップ

1. 必要なパッケージのインストール

# TypeScriptとts-loaderをインストール
npm install --save-dev typescript ts-loader

# 型定義ファイル(必要に応じて)
npm install --save-dev @types/node

2. 基本的な Webpack 設定

const path = require("path");

module.exports = {
  // TypeScriptファイルをエントリーポイントとして指定
  entry: "./src/index.ts",

  module: {
    rules: [
      {
        // .ts および .tsx ファイルを対象とする
        test: /\.tsx?$/,
        // ts-loaderを使用してTypeScriptを処理
        use: "ts-loader",
        // node_modules内のファイルは除外(ビルド高速化)
        exclude: /node_modules/,
      },
    ],
  },

  resolve: {
    // インポート時に拡張子を省略できるようにする
    extensions: [".tsx", ".ts", ".js"],
  },

  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, "dist"),
  },
};

ts-loader の詳細オプション

1. 基本オプション設定

module.exports = {
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: {
          loader: "ts-loader",
          options: {
            // TypeScript設定ファイルを指定(デフォルト: tsconfig.json)
            configFile: "tsconfig.json",

            // 型チェックをスキップ(高速化、別途型チェックツールを使用)
            transpileOnly: false,

            // エラー時にビルドを継続するかどうか
            ignoreDiagnostics: [2307], // 特定のエラーコードを無視

            // コンパイラーオプションを上書き
            compilerOptions: {
              sourceMap: true,
              declaration: false,
            },
          },
        },
        exclude: /node_modules/,
      },
    ],
  },
};

2. 高速化のための transpileOnly モード

// 高速ビルド用設定(型チェックなし)
{
  test: /\.tsx?$/,
  use: {
    loader: 'ts-loader',
    options: {
      // 型チェックを無効化してトランスパイルのみ実行
      transpileOnly: true,

      // 実験的な高速化機能を有効化
      experimentalWatchApi: true,

      // ファイル変更検知の最適化
      useCaseSensitiveFileNames: true,
    },
  },
  exclude: /node_modules/,
}

TypeScript 設定との連携

tsconfig.json の最適化

{
  "compilerOptions": {
    // Webpack用の出力設定
    "target": "ES2020", // モダンブラウザ対応
    "module": "ESNext", // Webpackがモジュール解決を処理
    "moduleResolution": "node", // Node.js形式のモジュール解決

    // 型安全性の設定
    "strict": true, // 厳格な型チェック
    "noImplicitAny": true, // any型の暗黙的使用を禁止
    "strictNullChecks": true, // null/undefinedの厳格チェック

    // Webpack連携用設定
    "esModuleInterop": true, // CommonJSモジュールとの互換性
    "allowSyntheticDefaultImports": true, // デフォルトインポートを許可
    "skipLibCheck": true, // 型定義ファイルの型チェックをスキップ

    // 開発体験向上
    "sourceMap": true, // ソースマップを生成
    "declaration": true, // 型定義ファイルを生成
    "declarationMap": true, // 型定義ファイルのソースマップ

    // パス解決
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"]
    }
  },

  // コンパイル対象ファイル
  "include": ["src/**/*", "types/**/*"],

  // コンパイル除外ファイル
  "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
}

パフォーマンス最適化

1. インクリメンタルコンパイル

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: {
          loader: "ts-loader",
          options: {
            // インクリメンタルコンパイルを有効化
            transpileOnly: true,
            experimentalWatchApi: true,
          },
        },
        exclude: /node_modules/,
      },
    ],
  },

  // キャッシュを有効化(Webpack 5)
  cache: {
    type: "filesystem",
    buildDependencies: {
      config: [__filename],
    },
  },
};

2. 型チェックの分離(ForkTsCheckerWebpackPlugin 使用)

npm install --save-dev fork-ts-checker-webpack-plugin
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");

module.exports = {
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: {
          loader: "ts-loader",
          options: {
            // 型チェックを無効化(ForkTsCheckerが別途実行)
            transpileOnly: true,
          },
        },
        exclude: /node_modules/,
      },
    ],
  },

  plugins: [
    // 型チェックを別プロセスで並行実行
    new ForkTsCheckerWebpackPlugin({
      // 非同期で型チェックを実行
      async: false,

      // ESLintも同時に実行
      eslint: {
        files: "./src/**/*.{ts,tsx,js,jsx}",
      },

      // TypeScript設定ファイルを指定
      typescript: {
        configFile: "tsconfig.json",
      },
    }),
  ],
};

実際のプロジェクト例

プロジェクト構造

my-typescript-project/
├── src/
│   ├── index.ts
│   ├── components/
│   │   └── Button.tsx
│   └── utils/
│       └── helpers.ts
├── dist/
├── tsconfig.json
├── webpack.config.js
└── package.json

src/index.ts

import { Button } from "@components/Button";
import { formatDate } from "@utils/helpers";

const app = document.getElementById("app");
if (app) {
  app.innerHTML = `
    <h1>Hello TypeScript + Webpack!</h1>
    <p>Today is: ${formatDate(new Date())}</p>
  `;
}

src/components/Button.tsx

import React from "react";

interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: "primary" | "secondary";
}

export const Button: React.FC<ButtonProps> = ({
  label,
  onClick,
  variant = "primary",
}) => {
  return (
    <button className={`btn btn-${variant}`} onClick={onClick}>
      {label}
    </button>
  );
};

src/utils/helpers.ts

export const formatDate = (date: Date): string => {
  return date.toLocaleDateString("ja-JP", {
    year: "numeric",
    month: "long",
    day: "numeric",
  });
};

export const debounce = <T extends (...args: any[]) => any>(
  func: T,
  delay: number
): ((...args: Parameters<T>) => void) => {
  let timeoutId: NodeJS.Timeout;

  return (...args: Parameters<T>) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => func(...args), delay);
  };
};

トラブルシューティング

よくある問題と解決方法

1. モジュール解決エラー

# エラー例: Cannot find module '@components/Button'

解決方法:

// webpack.config.js
resolve: {
  extensions: ['.tsx', '.ts', '.js'],
  alias: {
    '@components': path.resolve(__dirname, 'src/components'),
    '@utils': path.resolve(__dirname, 'src/utils'),
  },
}

// tsconfig.json
"paths": {
  "@components/*": ["src/components/*"],
  "@utils/*": ["src/utils/*"]
}

2. 型定義ファイルが見つからない

# エラー例: Could not find a declaration file for module 'some-library'

解決方法:

# 型定義ファイルをインストール
npm install --save-dev @types/some-library

# または、カスタム型定義を作成
# types/some-library.d.ts
declare module 'some-library' {
  export function someFunction(): void;
}

3. ビルド時間が遅い

解決方法:

// 高速化設定
{
  loader: 'ts-loader',
  options: {
    transpileOnly: true,        // 型チェックを無効化
    experimentalWatchApi: true, // 実験的な高速API
  },
}

// + ForkTsCheckerWebpackPluginで型チェックを分離

webpack-dev-server による開発環境の構築

webpack-dev-serverは、開発時に高速で効率的な開発体験を提供する Webpack の開発サーバーです。ファイルの変更を監視し、自動的にブラウザをリロードしたり、Hot Module Replacement(HMR)によってページをリロードせずに変更を反映できます。

webpack-dev-server とは

webpack-dev-server は以下の機能を提供します:

  • ライブリロード: ファイル変更時の自動ブラウザ更新
  • Hot Module Replacement (HMR): ページをリロードせずに変更部分のみを更新
  • メモリ内ビルド: ファイルシステムに書き込まずメモリ上でビルド(高速)
  • プロキシ機能: API サーバーへのリクエストをプロキシ
  • 静的ファイル配信: 開発用の静的ファイルサーバー

インストールと基本設定

1. 必要なパッケージのインストール

# webpack-dev-serverをインストール
npm install --save-dev webpack-dev-server

# HTMLファイル生成用プラグイン(通常必要)
npm install --save-dev html-webpack-plugin

2. 基本的な Webpack 設定

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  // 開発モードに設定
  mode: "development",

  entry: "./src/index.ts",

  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: "ts-loader",
        exclude: /node_modules/,
      },
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"],
      },
    ],
  },

  resolve: {
    extensions: [".tsx", ".ts", ".js"],
  },

  // 開発サーバーの設定
  devServer: {
    // 静的ファイルを提供するディレクトリ
    static: {
      directory: path.join(__dirname, "dist"),
      // 静的ファイルの監視を有効化
      watch: true,
    },

    // サーバーのポート番号
    port: 3000,

    // サーバー起動時にブラウザを自動で開く
    open: true,

    // Hot Module Replacementを有効化
    hot: true,

    // ファイル変更時の自動リロード
    liveReload: true,

    // エラーをブラウザのオーバーレイ表示
    client: {
      overlay: {
        errors: true,
        warnings: false,
      },
    },

    // 詳細なログ出力を制御
    devMiddleware: {
      writeToDisk: false, // ファイルをディスクに書き込まない(メモリ上で処理)
    },
  },

  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html",
      title: "TypeScript + Webpack Dev Server",
    }),
  ],

  // 開発用の出力設定
  output: {
    filename: "[name].bundle.js",
    path: path.resolve(__dirname, "dist"),
    // 開発時はファイルをクリーンアップしない
    clean: false,
  },
};

詳細な設定オプション

1. サーバー基本設定

devServer: {
  // ホスト名の設定(0.0.0.0で外部からアクセス可能)
  host: 'localhost', // または '0.0.0.0'

  // ポート番号(自動で空いているポートを使用)
  port: 'auto', // または具体的な番号

  // HTTPS を有効化
  https: false, // または true、証明書オブジェクト

  // IPv6 を有効化
  allowedHosts: 'all', // またはホスト名の配列

  // サーバー起動時の動作
  open: {
    target: ['http://localhost:3000'],
    app: {
      name: 'google chrome', // 使用するブラウザ
    },
  },
}

2. Hot Module Replacement (HMR) の詳細設定

devServer: {
  // HMRを有効化
  hot: true,

  // HMR失敗時のフォールバック動作
  liveReload: true,
}

// HMRを使用するTypeScriptコード例
// src/index.ts
if (module.hot) {
  // モジュールが更新された時の処理
  module.hot.accept('./components/App', () => {
    console.log('App component updated!');
    // アプリケーションの再レンダリング処理
  });

  // HMRの状態変更を監視
  module.hot.addStatusHandler(status => {
    console.log('HMR Status:', status);
  });
}

3. プロキシ設定(API サーバー連携)

devServer: {
  // APIサーバーへのプロキシ設定
  proxy: [
    {
      // /api/* のリクエストをプロキシ
      context: ['/api'],
      target: 'http://localhost:8080',
      // オリジンヘッダーを変更
      changeOrigin: true,
      // パスを書き換え(/api/users → /users)
      pathRewrite: {
        '^/api': '',
      },
      // ログを有効化
      logLevel: 'debug',
    },
    {
      // WebSocketのプロキシ
      context: ['/ws'],
      target: 'ws://localhost:8080',
      ws: true,
    },
  ],
}

// 使用例:フロントエンドからのAPIコール
// fetch('/api/users') → http://localhost:8080/users にプロキシされる

4. 静的ファイルの配信設定

devServer: {
  static: [
    {
      // メインの静的ファイルディレクトリ
      directory: path.join(__dirname, 'dist'),
      // publicPath: ブラウザからアクセスする際のURLパス
      publicPath: '/',
      watch: true,
    },
    {
      // 追加の静的ファイルディレクトリ(画像など)
      directory: path.join(__dirname, 'public'),
      // /assets でアクセス可能にする
      publicPath: '/assets',
      watch: false,
    },
  ],
}

publicPath の詳細解説

publicPathは、Webpack において非常に重要な概念で、ブラウザからアセットにアクセスする際のベース URL パスを指定します。ファイルシステム上の実際のパスと、ブラウザからアクセスする際の URL パスを関連付ける役割を持ちます。

基本的な仕組み

// ファイルシステム上のパス vs ブラウザからのアクセスパス
{
  directory: '/Users/username/project/public',  // 実際のファイルパス
  publicPath: '/assets',                        // ブラウザからのアクセスパス
}

// 結果:
// ファイル: /Users/username/project/public/images/logo.png
// ブラウザ: http://localhost:3000/assets/images/logo.png

webpack-dev-server での publicPath 設定例

devServer: {
  static: [
    {
      // ケース1: ルートパスで配信
      directory: path.join(__dirname, 'dist'),
      publicPath: '/',
      // http://localhost:3000/bundle.js でアクセス可能
    },
    {
      // ケース2: サブディレクトリで配信
      directory: path.join(__dirname, 'public'),
      publicPath: '/static',
      // public/favicon.ico → http://localhost:3000/static/favicon.ico
    },
    {
      // ケース3: 複数レベルのパス
      directory: path.join(__dirname, 'assets'),
      publicPath: '/app/assets',
      // assets/images/logo.png → http://localhost:3000/app/assets/images/logo.png
    },
  ],
}

output.publicPath との関係

module.exports = {
  output: {
    // バンドルファイルのpublicPath(Webpackが生成するファイル用)
    publicPath: "/dist/",
    filename: "bundle.js",
    path: path.resolve(__dirname, "dist"),
  },

  devServer: {
    static: {
      // 静的ファイルのpublicPath(既存ファイル用)
      directory: path.join(__dirname, "public"),
      publicPath: "/assets",
    },
  },
};

// 結果:
// Webpackが生成: http://localhost:3000/dist/bundle.js
// 静的ファイル: http://localhost:3000/assets/favicon.ico

実際の使用シナリオ

シナリオ 1: CDN 使用時

// 本番環境でCDNを使用する場合
module.exports = {
  output: {
    // 本番環境では絶対URLを指定
    publicPath:
      process.env.NODE_ENV === "production"
        ? "https://cdn.example.com/assets/"
        : "/assets/",
  },

  devServer: {
    static: {
      directory: path.join(__dirname, "public"),
      // 開発環境では相対パス
      publicPath: "/assets",
    },
  },
};

// 開発環境: http://localhost:3000/assets/bundle.js
// 本番環境: https://cdn.example.com/assets/bundle.js

シナリオ 2: サブディレクトリでの運用

// アプリが https://example.com/myapp/ で動作する場合
module.exports = {
  output: {
    publicPath: "/myapp/",
  },

  devServer: {
    static: {
      directory: path.join(__dirname, "public"),
      publicPath: "/myapp/static",
    },
  },
};

// HTML内で生成されるパス:
// <script src="/myapp/bundle.js"></script>
// <img src="/myapp/static/logo.png" />

publicPath 設定時の注意点

1. 末尾のスラッシュ

// ✅ 推奨: 末尾にスラッシュを付ける
publicPath: "/assets/";

// ❌ 問題の可能性: スラッシュなし
publicPath: "/assets";

// 結果の違い:
// 正しい: /assets/image.png
// 間違い: /assetsimage.png (結合されてしまう可能性)

2. 絶対パス vs 相対パス

// 絶対パス(推奨)
publicPath: "/assets/"; // ルートからの絶対パス
publicPath: "https://cdn..."; // 完全なURL

// 相対パス(特殊なケースでのみ使用)
publicPath: "../assets/"; // 現在のページからの相対パス
publicPath: "assets/"; // 現在のディレクトリからの相対パス

3. HTML での参照例

<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <!-- Webpackが自動生成するscriptタグ -->
    <script src="/dist/bundle.js"></script>
  </head>
  <body>
    <!-- 静的ファイルへの参照 -->
    <img src="/assets/images/logo.png" alt="Logo" />
    <link rel="stylesheet" href="/assets/styles/theme.css" />
  </body>
</html>

デバッグ用の設定

devServer: {
  static: [
    {
      directory: path.join(__dirname, 'public'),
      publicPath: '/debug-assets',
      // ログを有効化してアクセス状況を確認
      serveIndex: true,  // ディレクトリ一覧を表示
    },
  ],

  // ミドルウェアでpublicPathの動作を確認
  devMiddleware: {
    writeToDisk: true,  // ファイルを実際に書き出して確認
  },

  // 詳細なログ出力
  client: {
    logging: 'verbose',
  },
}

よくある間違いと解決方法

間違い 1: publicPath の設定忘れ

// ❌ 問題のある設定
devServer: {
  static: {
    directory: path.join(__dirname, 'assets'),
    // publicPath未設定 → デフォルトで'/'になる
  },
}

// ✅ 正しい設定
devServer: {
  static: {
    directory: path.join(__dirname, 'assets'),
    publicPath: '/assets',  // 明示的に指定
  },
}

間違い 2: output.publicPath との混同

// これらは別物!
module.exports = {
  output: {
    publicPath: "/build/", // Webpackが生成するファイル用
  },
  devServer: {
    static: {
      publicPath: "/static", // 既存の静的ファイル用
    },
  },
};

**5. クライアント側の設定**

```javascript
devServer: {
  client: {
    // WebSocket接続の設定
    webSocketURL: 'ws://localhost:3000/ws',

    // エラーオーバーレイの設定
    overlay: {
      errors: true,    // エラーを表示
      warnings: false, // 警告は非表示
      runtimeErrors: true, // ランタイムエラーも表示
    },

    // 進捗表示
    progress: true,

    // 再接続の試行回数
    reconnect: 5,

    // ログレベル
    logging: 'info', // 'none', 'error', 'warn', 'info', 'log', 'verbose'
  },
}

package.json のスクリプト設定

{
  "scripts": {
    // 基本的な開発サーバー起動
    "dev": "webpack serve --mode development",

    // 設定ファイルを指定して起動
    "start": "webpack serve --config webpack.dev.js",

    // オープンを無効化して起動
    "dev:no-open": "webpack serve --no-open",

    // 特定のポートで起動
    "dev:port": "webpack serve --port 4000",

    // HTTPS で起動
    "dev:https": "webpack serve --https",

    // 外部からアクセス可能にして起動
    "dev:host": "webpack serve --host 0.0.0.0",

    // 本番ビルド
    "build": "webpack --mode production"
  }
}

環境別設定の分離

webpack.dev.js(開発環境専用設定)

const { merge } = require("webpack-merge");
const common = require("./webpack.common.js");

module.exports = merge(common, {
  mode: "development",

  // 開発用のソースマップ(高速)
  devtool: "eval-source-map",

  devServer: {
    static: "./dist",
    port: 3000,
    hot: true,
    open: true,

    // 開発専用の設定
    devMiddleware: {
      // 統計情報の表示レベル
      stats: "minimal",
    },

    // ヘッダーの追加
    headers: {
      "Access-Control-Allow-Origin": "*",
    },

    // 履歴API対応(SPA用)
    historyApiFallback: {
      // 404時にindex.htmlを返す
      rewrites: [{ from: /^\/api/, to: "/404.html" }],
    },
  },

  // 開発用の最適化設定
  optimization: {
    // ランタイムチャンクを分離(HMR高速化)
    runtimeChunk: "single",
  },
});

TypeScript + React での HMR 設定例

1. React Refresh Webpack Plugin の設定

npm install --save-dev @pmmmwh/react-refresh-webpack-plugin react-refresh
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");

module.exports = {
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: {
          loader: "ts-loader",
          options: {
            transpileOnly: true,
            getCustomTransformers: () => ({
              before: [require("react-refresh-typescript")()],
            }),
          },
        },
        exclude: /node_modules/,
      },
    ],
  },

  plugins: [
    // React Fast Refresh を有効化
    new ReactRefreshWebpackPlugin(),
  ],

  devServer: {
    hot: true,
  },
};

2. React コンポーネント例

// src/components/Counter.tsx
import React, { useState } from "react";

export const Counter: React.FC = () => {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h2>Counter: {count}</h2>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
    </div>
  );
};

トラブルシューティング

よくある問題と解決方法

1. HMR が動作しない

// 解決方法
devServer: {
  hot: true,
  liveReload: true, // フォールバックとして有効化
}

// TypeScriptの場合、型定義を追加
// types/global.d.ts
declare global {
  interface NodeModule {
    hot?: {
      accept: (dependencies?: string | string[], callback?: () => void) => void;
      addStatusHandler: (handler: (status: string) => void) => void;
    };
  }
}

2. ポートが既に使用されている

# エラー: Port 3000 is already in use
// 解決方法
devServer: {
  port: 'auto', // 自動でポートを選択
}

// または、特定のポートを指定
devServer: {
  port: 4000,
}

3. 外部デバイスからアクセスできない

// 解決方法
devServer: {
  host: '0.0.0.0', // 全てのインターフェースでリッスン
  allowedHosts: 'all', // 全てのホストを許可
}

4. プロキシが動作しない

// デバッグ用設定
devServer: {
  proxy: {
    '/api': {
      target: 'http://localhost:8080',
      changeOrigin: true,
      logLevel: 'debug', // 詳細なログを出力
      onProxyReq: (proxyReq, req, res) => {
        console.log('Proxying request:', req.url);
      },
    },
  },
}

パフォーマンス最適化

1. 大規模プロジェクト用設定

devServer: {
  // ファイル監視の最適化
  watchFiles: {
    paths: ['src/**/*'],
    options: {
      usePolling: false, // ポーリングを無効化(CPUを節約)
      ignored: /node_modules/, // node_modulesを監視対象外
    },
  },

  // コンパイル時の最適化
  devMiddleware: {
    writeToDisk: false,
    stats: 'errors-warnings-only', // エラーと警告のみ表示
  },
}

2. メモリ使用量の最適化

// webpack.config.js
module.exports = {
  // キャッシュ設定
  cache: {
    type: "filesystem",
    cacheDirectory: path.resolve(__dirname, ".webpack-cache"),
  },

  // 最適化設定
  optimization: {
    removeAvailableModules: false,
    removeEmptyChunks: false,
    splitChunks: false,
  },
};

まとめ

webpack-dev-server は、TypeScript + Webpack での開発効率を大幅に向上させる強力なツールです。適切な設定により、高速なビルド、自動リロード、HMR などの機能を活用して、快適な開発環境を構築できます。プロジェクトの規模や要件に応じて、設定を調整することが重要です。

プロジェクトの初期化

mkdir typescript-webpack-project
cd typescript-webpack-project
npm init -y

必要なパッケージのインストール

# TypeScript関連
npm install --save-dev typescript ts-loader

# Webpack関連
npm install --save-dev webpack webpack-cli webpack-dev-server

# その他の便利なプラグイン
npm install --save-dev html-webpack-plugin clean-webpack-plugin

TypeScript 設定ファイル(tsconfig.json)

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "lib": ["ES2020", "DOM"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "sourceMap": true,
    "declaration": true,
    "declarationMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

パスの扱いについて重要な解説

Webpack の設定でよく見かけるpath.resolve(__dirname, 'dist')について詳しく説明します。

path.resolve()の役割

const path = require("path");

// 基本的な使い方
path.resolve(__dirname, "dist");
// 結果例: '/Users/username/myproject/dist' (macOS/Linux)
// 結果例: 'C:\Users\username\myproject\dist' (Windows)

なぜ絶対パスが必要なのか?

Webpack のoutput.pathプロパティは絶対パスを要求します。相対パス(./dist)では正しく動作しません:

// ❌ 動作しない例
module.exports = {
  output: {
    path: "./dist", // 相対パスは使用できない
  },
};

// ✅ 正しい例
module.exports = {
  output: {
    path: path.resolve(__dirname, "dist"), // 絶対パスを生成
  },
};

各要素の詳細解説

1. __dirname(現在のファイルが存在するディレクトリの絶対パス)

// webpack.config.jsが /Users/username/myproject/ にある場合
console.log(__dirname);
// 出力: '/Users/username/myproject'

// プロジェクト構造例
myproject/
├── webpack.config.js   __dirnameはここのパス
├── src/
└── dist/

2. path.resolve()(複数のパスセグメントを結合して絶対パスを作成)

// 複数の引数を結合
path.resolve("/users", "john", "documents", "file.txt");
// 結果: '/users/john/documents/file.txt'

// 相対パスから絶対パスへ変換
path.resolve("dist");
// 結果: '/current/working/directory/dist'

// __dirnameとの組み合わせ
path.resolve(__dirname, "dist");
// 結果: '/Users/username/myproject/dist'

クロスプラットフォーム対応

Node のpathモジュールを使用することで、OS 間のパス区切り文字の違いを自動的に処理できます:

// 手動でパスを結合(❌ 推奨されない)
const outputPath = __dirname + "/dist"; // Windowsで問題が発生

// path.resolve()を使用(✅ 推奨)
const outputPath = path.resolve(__dirname, "dist"); // どのOSでも正しく動作

OS 別のパス区切り文字:

  • Windows: \ (バックスラッシュ)
  • macOS/Linux: / (スラッシュ)

実際の使用例

const path = require("path");

module.exports = {
  // エントリーポイントの絶対パス指定
  entry: path.resolve(__dirname, "src", "index.ts"),

  output: {
    // 出力ディレクトリの絶対パス(Webpackの要求事項)
    path: path.resolve(__dirname, "dist"),
    filename: "bundle.js",
  },

  resolve: {
    alias: {
      // エイリアスも絶対パスで指定することが多い
      "@": path.resolve(__dirname, "src"),
      "@components": path.resolve(__dirname, "src", "components"),
      "@utils": path.resolve(__dirname, "src", "utils"),
    },
  },

  plugins: [
    new HtmlWebpackPlugin({
      // テンプレートファイルの絶対パス
      template: path.resolve(__dirname, "src", "index.html"),
    }),
  ],
};

よくある間違いと解決方法

間違い 1: 相対パスの使用

// ❌ エラーになる
output: {
  path: './dist',
}

// ✅ 正しい
output: {
  path: path.resolve(__dirname, 'dist'),
}

間違い 2: 文字列結合でのパス作成

// ❌ OSによっては動作しない
const distPath = __dirname + "/dist";

// ✅ クロスプラットフォーム対応
const distPath = path.resolve(__dirname, "dist");

間違い 3: path モジュールのインポート忘れ

// ❌ pathが未定義
module.exports = {
  output: {
    path: path.resolve(__dirname, "dist"), // ReferenceError
  },
};

// ✅ pathモジュールをインポート
const path = require("path");
module.exports = {
  output: {
    path: path.resolve(__dirname, "dist"),
  },
};

Webpack 設定ファイル(webpack.config.js)

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

module.exports = {
  // エントリーポイント: バンドルの開始点となるファイルを指定
  // './src/index.ts'を起点に依存関係を解析してバンドルを作成
  entry: "./src/index.ts",

  // モジュールの処理ルールを定義
  module: {
    rules: [
      {
        // test: 処理対象のファイルを正規表現で指定(.ts、.tsxファイル)
        test: /\.tsx?$/,
        // use: 使用するローダーを指定(TypeScriptファイルをJavaScriptに変換)
        use: "ts-loader",
        // exclude: 処理から除外するディレクトリ(node_modulesを除外してビルド高速化)
        exclude: /node_modules/,
      },
      {
        // CSSファイルの処理ルール
        test: /\.css$/i,
        // 複数のローダーを配列で指定(右から左の順で実行)
        // css-loader: CSSファイルを解析、style-loader: スタイルをDOMに注入
        use: ["style-loader", "css-loader"],
      },
    ],
  },

  // モジュール解決の設定
  resolve: {
    // 拡張子を省略してimportできるファイル拡張子を指定
    // import './component'で.tsx、.ts、.jsファイルを自動検索
    extensions: [".tsx", ".ts", ".js"],
  },

  // 出力設定
  output: {
    // 出力ファイル名([contenthash]でファイル内容に基づくハッシュ値を追加)
    // キャッシュ戦略に有効(ファイル内容が変わらなければハッシュも同じ)
    filename: "bundle.[contenthash].js",
    // 出力先ディレクトリの絶対パス
    path: path.resolve(__dirname, "dist"),
  },

  // プラグインの設定(ビルドプロセスを拡張する機能)
  plugins: [
    // ビルド前にdistディレクトリをクリーンアップ
    new CleanWebpackPlugin(),
    // HTMLファイルを自動生成し、生成されたJSファイルを自動で読み込み
    new HtmlWebpackPlugin({
      // テンプレートHTMLファイルを指定
      template: "./src/index.html",
    }),
  ],

  // 開発サーバーの設定
  devServer: {
    // 静的ファイルを提供するディレクトリ
    static: "./dist",
    // 開発サーバーのポート番号
    port: 3000,
    // ブラウザを自動で開く
    open: true,
  },
};

開発環境と本番環境の分離

webpack.common.js

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

module.exports = {
  // アプリケーションのエントリーポイント
  entry: "./src/index.ts",

  // ローダーの設定
  module: {
    rules: [
      {
        // TypeScriptファイル(.ts、.tsx)の処理ルール
        test: /\.tsx?$/,
        // ts-loaderでTypeScriptをJavaScriptに変換
        use: "ts-loader",
        // node_modulesディレクトリは処理対象外
        exclude: /node_modules/,
      },
    ],
  },

  // モジュール解決の設定
  resolve: {
    // インポート時に省略可能な拡張子
    extensions: [".tsx", ".ts", ".js"],
  },

  // 共通で使用するプラグイン
  plugins: [
    // ビルド前にoutputディレクトリをクリーンアップ
    new CleanWebpackPlugin(),
    // 指定したテンプレートからHTMLファイルを生成
    new HtmlWebpackPlugin({
      template: "./src/index.html",
    }),
  ],
};

webpack.dev.js

const { merge } = require("webpack-merge");
const common = require("./webpack.common.js");

module.exports = merge(common, {
  // 開発モード(最適化は最小限、デバッグ情報優先)
  mode: "development",
  // ソースマップの種類(開発時は高速なinline-source-mapを使用)
  devtool: "inline-source-map",

  // 開発サーバーの設定
  devServer: {
    // 静的ファイルの提供ディレクトリ
    static: "./dist",
    // ポート番号
    port: 3000,
    // Hot Module Replacement(変更時の自動リロード)
    hot: true,
    // ブラウザの自動起動
    open: true,
  },

  // 出力設定(開発時はシンプルなファイル名)
  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, "dist"),
  },
});

webpack.prod.js

const { merge } = require("webpack-merge");
const common = require("./webpack.common.js");

module.exports = merge(common, {
  // 本番モード(圧縮・最適化を実行)
  mode: "production",
  // 本番用ソースマップ(デバッグ可能だが軽量)
  devtool: "source-map",

  // 本番用出力設定
  output: {
    // コンテンツハッシュ付きファイル名(キャッシュ戦略のため)
    filename: "bundle.[contenthash].js",
    path: path.resolve(__dirname, "dist"),
    // 出力前にディレクトリをクリーンアップ
    clean: true,
  },

  // 最適化設定
  optimization: {
    // コード分割の設定
    splitChunks: {
      // すべてのチャンク(同期・非同期)を分割対象とする
      chunks: "all",
    },
  },
});

package.json スクリプトの設定

{
  "scripts": {
    "start": "webpack serve --config webpack.dev.js",
    "build": "webpack --config webpack.prod.js",
    "type-check": "tsc --noEmit",
    "lint": "eslint src/**/*.{ts,tsx}",
    "test": "jest"
  }
}

高度な設定とベストプラクティス

パス解決の最適化

// webpack.config.js
module.exports = {
  resolve: {
    // インポート時に省略可能な拡張子を指定
    extensions: [".tsx", ".ts", ".js"],
    // パスエイリアスの設定(長い相対パスを短縮)
    alias: {
      // '@' でsrcディレクトリを参照可能
      "@": path.resolve(__dirname, "src"),
      // '@components'でcomponentsディレクトリを直接参照
      "@components": path.resolve(__dirname, "src/components"),
      // '@utils'でutilsディレクトリを直接参照
      "@utils": path.resolve(__dirname, "src/utils"),
    },
  },
};

型チェックの分離

ts-loader は型チェックとトランスパイルを同時に行うため、ビルド時間が長くなることがあります。ForkTsCheckerWebpackPlugin を使用することで、型チェックを別プロセスで並行実行できます。

npm install --save-dev fork-ts-checker-webpack-plugin
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");

module.exports = {
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: {
          loader: "ts-loader",
          options: {
            // 型チェックを無効化(高速化のため)
            transpileOnly: true,
          },
        },
        exclude: /node_modules/,
      },
    ],
  },

  plugins: [
    // 型チェックを別プロセスで並行実行
    new ForkTsCheckerWebpackPlugin({
      // 同期的に型チェックを実行(エラー時にビルドを停止)
      async: false,
    }),
  ],
};

CSS Modules の設定

module.exports = {
  module: {
    rules: [
      {
        // .module.cssファイルをCSS Modulesとして処理
        test: /\.module\.css$/,
        use: [
          // スタイルをDOMに注入
          "style-loader",
          {
            loader: "css-loader",
            options: {
              // CSS Modulesを有効化
              modules: {
                // クラス名の生成パターン(ローカル名__ハッシュ値)
                localIdentName: "[local]__[hash:base64:5]",
              },
            },
          },
        ],
      },
    ],
  },
};

型定義ファイルの生成

TypeScript ライブラリを開発する場合は、型定義ファイルの生成も重要です。

// tsconfig.json
{
  "compilerOptions": {
    "declaration": true,
    "declarationMap": true,
    "outDir": "./dist"
  }
}

パフォーマンス最適化

Tree Shaking の有効化

// webpack.config.js
module.exports = {
  // 本番モードでTree Shakingを有効化
  mode: "production",
  optimization: {
    // 使用されているエクスポートのみを含める
    usedExports: true,
    // サイドエフェクトがないことを明示(全モジュールでTree Shaking有効)
    sideEffects: false,
  },
};

コード分割

module.exports = {
  optimization: {
    // チャンク分割の設定
    splitChunks: {
      // すべてのチャンク(同期・非同期)を分割対象とする
      chunks: "all",
      cacheGroups: {
        // node_modulesのライブラリを別チャンクとして分離
        vendor: {
          // node_modulesディレクトリのファイルを対象
          test: /[\\/]node_modules[\\/]/,
          // 出力ファイル名
          name: "vendors",
          // 全てのチャンクを対象
          chunks: "all",
        },
      },
    },
  },
};

トラブルシューティング

よくある問題と解決方法

モジュールが見つからないエラー

  • resolve.extensionsにファイル拡張子が含まれているか確認
  • パスエイリアスが正しく設定されているか確認

型エラーが表示されない

  • ForkTsCheckerWebpackPluginが正しく設定されているか確認
  • transpileOnly: true設定時は型チェックプラグインが必要

ビルド時間が遅い

  • transpileOnly: trueの使用を検討
  • キャッシュの有効化(cache: true
  • 不要なファイルを exclude に追加

まとめ

TypeScript と Webpack の組み合わせは、型安全で効率的な Web アプリケーション開発を可能にします。基本的な設定から始めて、プロジェクトの要件に応じて段階的に最適化を行うことが重要です。

この設定を基盤として、React、Vue.js、Angular などのフレームワークとの連携や、より高度な最適化手法を導入することで、さらに強力な開発環境を構築できます。

継続的な学習と実践を通じて、モダンな Web アプリケーション開発のスキルを向上させていきましょう。

TypeScript で JavaScript ライブラリを使うガイド

TypeScript プロジェクトで既存の JavaScript ライブラリを使用する際、型安全性を保ちながら効率的に開発する方法を解説します。

1. 型定義ファイルの利用

DefinitelyTyped(@types)を使用する方法

最も一般的で推奨される方法です。多くの人気ライブラリには、コミュニティによって型定義が提供されています。

# ライブラリと型定義を同時にインストール
npm install lodash
npm install -D @types/lodash

# または一行で
npm install lodash && npm install -D @types/lodash
import _ from "lodash";

// 型安全にlodashを使用できる
const users = [
  { name: "Alice", age: 25 },
  { name: "Bob", age: 30 },
];

const sortedUsers = _.sortBy(users, "age"); // 型推論が効く
console.log(sortedUsers);

型定義の確認方法

# 利用可能な型定義を検索
npm search @types/ライブラリ名

# または公式サイトで確認
# https://www.typescriptlang.org/dt/search

2. declare キーワードの理解

declare とは

declareキーワードは、TypeScript において「すでに存在する」変数、関数、クラス、モジュールなどの型情報を宣言するために使用されます。実際の実装は提供せず、型情報のみを定義します。

declare の基本的な使い方

// 変数の宣言
declare const API_URL: string;
declare let globalConfig: { debug: boolean };

// 関数の宣言
declare function log(message: string): void;
declare function processData(data: any[]): Promise<any>;

// クラスの宣言
declare class ExternalLibrary {
  constructor(config: object);
  method1(): string;
  method2(param: number): boolean;
}

// インターフェイスの宣言(declareは不要)
interface Config {
  apiKey: string;
  timeout: number;
}

declare vs 通常の宣言の違い

// 通常の宣言(実装を含む)
function normalFunction(x: number): number {
  return x * 2; // 実装がある
}

// declare宣言(型情報のみ)
declare function externalFunction(x: number): number;
// 実装は別の場所(JSファイルやCDNなど)に存在

// 使用時
const result1 = normalFunction(5); // TypeScript内で実装
const result2 = externalFunction(5); // 外部の実装を使用

declare global の使用

グローバルスコープに存在する変数や関数を宣言する場合:

declare global {
  // window オブジェクトの拡張
  interface Window {
    myGlobalVar: string;
    myGlobalFunction(): void;
  }

  // グローバル変数
  var API_BASE_URL: string;
  var DEBUG_MODE: boolean;

  // グローバル関数
  function globalUtility(param: string): number;
}

// 使用例
window.myGlobalVar = "Hello";
globalUtility("test");

declare module の使用

外部モジュールの型定義:

// 完全なモジュール宣言
declare module "external-library" {
  export interface Config {
    apiKey: string;
    endpoint: string;
  }

  export class Client {
    constructor(config: Config);
    send(data: any): Promise<any>;
  }

  export function createClient(config: Config): Client;

  // デフォルトエクスポート
  export default Client;
}

// ワイルドカード宣言(緊急時用)
declare module "some-untyped-library" {
  const library: any;
  export default library;
}

// 特定のファイル形式の宣言
declare module "*.css" {
  const content: { [className: string]: string };
  export default content;
}

declare module "*.json" {
  const value: any;
  export default value;
}

declare namespace の使用

名前空間の宣言(主にグローバルライブラリ用):

declare namespace MyLibrary {
  interface Options {
    debug: boolean;
    theme: "light" | "dark";
  }

  interface Result {
    success: boolean;
    data: any;
  }

  function init(options: Options): void;
  function getData(): Promise<Result>;

  namespace Utils {
    function formatDate(date: Date): string;
    function parseJson(str: string): any;
  }
}

// 使用例
MyLibrary.init({ debug: true, theme: "dark" });
const formattedDate = MyLibrary.Utils.formatDate(new Date());

実践的な declare の例

// types/environment.d.ts
declare global {
  namespace NodeJS {
    interface ProcessEnv {
      NODE_ENV: "development" | "production" | "test";
      API_URL: string;
      DATABASE_URL: string;
      JWT_SECRET: string;
    }
  }
}

// types/legacy-library.d.ts
declare module "legacy-jquery-plugin" {
  interface JQuery {
    customPlugin(options?: {
      animation?: boolean;
      duration?: number;
      callback?: () => void;
    }): JQuery;
  }
}

// types/custom-globals.d.ts
declare global {
  // カスタムイベントの型定義
  interface WindowEventMap {
    "custom-event": CustomEvent<{ message: string }>;
  }

  // 外部ライブラリのグローバル変数
  const gtag: (
    command: "config" | "event",
    targetId: string,
    config?: object
  ) => void;
}

export {}; // モジュール化

declare を使う場面

  1. CDN ライブラリの型定義

    // scriptタグで読み込まれたライブラリ
    declare const Chart: any;
    
  2. 環境変数の型定義

    declare global {
      namespace NodeJS {
        interface ProcessEnv {
          MY_API_KEY: string;
        }
      }
    }
    
  3. レガシーコードとの連携

    // 既存のJavaScriptファイルの関数
    declare function legacyFunction(param: string): number;
    
  4. サードパーティライブラリの補完

    // 型定義が不完全なライブラリの拡張
    declare module "some-library" {
      export function missingFunction(): void;
    }
    

注意点とベストプラクティス

// ❌ 避けるべき例
declare var anything: any; // 型安全性を失う

// ✅ 推奨される例
declare var config: {
  apiUrl: string;
  timeout: number;
  retries: number;
};

// ❌ 過度に複雑な宣言
declare module "complex-library" {
  // 数百行の複雑な型定義...
}

// ✅ 必要最小限の宣言
declare module "simple-library" {
  export function doSomething(input: string): Promise<string>;
}

3. 型定義がない場合の対処法

基本的な型宣言

型定義がないライブラリに対して、最低限の型宣言を作成します。

// types/custom-library.d.ts
declare module "custom-library" {
  export function doSomething(param: string): number;
  export interface CustomConfig {
    option1: boolean;
    option2?: string;
  }
  export default class CustomLibrary {
    constructor(config: CustomConfig);
    method(): void;
  }
}
// 使用例
import CustomLibrary, { doSomething } from "custom-library";

const result = doSomething("test"); // number型として推論
const lib = new CustomLibrary({ option1: true });

any 型を使用した暫定的な解決法

開発速度を優先する場合の一時的な解決策です。

// types/vendors.d.ts
declare module "some-js-library" {
  const library: any;
  export default library;
}

// または
declare var someGlobalLibrary: any;

3. CommonJS ライブラリの扱い

import = require() 構文

古い CommonJS ライブラリを使用する場合:

// tsconfig.jsonで "esModuleInterop": true を設定
import express = require("express");
// または
import * as express from "express";

const app = express();

混在モジュールの処理

// ESModulesとCommonJSが混在する場合
import React from "react"; // ES Modules
import * as fs from "fs"; // CommonJS

4. CDN ライブラリの型定義

HTML での CDN ライブラリ読み込み

CDN から script タグで直接読み込むライブラリに対する型定義方法:

<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <!-- CDNライブラリの読み込み -->
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  </head>
  <body>
    <div id="app"></div>
    <script src="./dist/main.js"></script>
  </body>
</html>

Chart.js の型定義例

// types/chart.d.ts
declare global {
  namespace Chart {
    interface ChartConfiguration {
      type: string;
      data: ChartData;
      options?: ChartOptions;
    }

    interface ChartData {
      labels?: string[];
      datasets: ChartDataset[];
    }

    interface ChartDataset {
      label: string;
      data: number[];
      backgroundColor?: string | string[];
      borderColor?: string | string[];
      borderWidth?: number;
    }

    interface ChartOptions {
      responsive?: boolean;
      plugins?: {
        legend?: {
          display?: boolean;
        };
      };
      scales?: {
        [key: string]: {
          beginAtZero?: boolean;
        };
      };
    }

    class Chart {
      constructor(ctx: HTMLCanvasElement | string, config: ChartConfiguration);
      destroy(): void;
      update(): void;
    }
  }

  // グローバルのChart変数
  const Chart: typeof Chart.Chart;
}

export {}; // モジュール化のため
// 使用例
const ctx = document.getElementById("myChart") as HTMLCanvasElement;
const chart = new Chart(ctx, {
  type: "bar",
  data: {
    labels: ["Red", "Blue", "Yellow"],
    datasets: [
      {
        label: "My Dataset",
        data: [12, 19, 3],
        backgroundColor: "rgba(255, 99, 132, 0.2)",
        borderColor: "rgba(255, 99, 132, 1)",
        borderWidth: 1,
      },
    ],
  },
  options: {
    responsive: true,
    scales: {
      y: {
        beginAtZero: true,
      },
    },
  },
});

jQuery の型定義例

// types/jquery.d.ts
declare global {
  interface JQuery {
    // DOM操作
    addClass(className: string): JQuery;
    removeClass(className: string): JQuery;
    toggleClass(className: string): JQuery;
    hide(duration?: number): JQuery;
    show(duration?: number): JQuery;
    fadeIn(duration?: number): JQuery;
    fadeOut(duration?: number): JQuery;

    // イベント
    click(handler?: (event: Event) => void): JQuery;
    on(event: string, handler: (event: Event) => void): JQuery;
    off(event: string): JQuery;

    // Ajax
    load(url: string, callback?: () => void): JQuery;

    // 属性・プロパティ
    attr(name: string): string;
    attr(name: string, value: string): JQuery;
    prop(name: string): any;
    prop(name: string, value: any): JQuery;
    val(): string;
    val(value: string): JQuery;

    // CSS
    css(property: string): string;
    css(property: string, value: string): JQuery;
    css(properties: { [key: string]: string }): JQuery;

    // 寸法・位置
    width(): number;
    height(): number;
    offset(): { top: number; left: number };
  }

  interface JQueryStatic {
    (selector: string): JQuery;
    (element: HTMLElement): JQuery;
    (callback: () => void): void;

    // Ajax関数
    ajax(settings: {
      url: string;
      method?: string;
      data?: any;
      success?: (data: any) => void;
      error?: (xhr: any, status: string, error: string) => void;
    }): void;

    get(url: string, callback?: (data: any) => void): void;
    post(url: string, data?: any, callback?: (data: any) => void): void;
  }

  const $: JQueryStatic;
  const jQuery: JQueryStatic;
}

export {};
// 使用例
$(document).ready(() => {
  $("#myButton").click((event) => {
    $(event.target).addClass("clicked").fadeOut(500);
  });

  $.ajax({
    url: "/api/data",
    method: "GET",
    success: (data) => {
      console.log("Data received:", data);
    },
  });
});

Axios の型定義例(CDN 版)

// types/axios-cdn.d.ts
declare global {
  namespace axios {
    interface AxiosRequestConfig {
      url?: string;
      method?: string;
      data?: any;
      headers?: { [key: string]: string };
      timeout?: number;
    }

    interface AxiosResponse<T = any> {
      data: T;
      status: number;
      statusText: string;
      headers: { [key: string]: string };
    }

    interface AxiosError {
      message: string;
      response?: AxiosResponse;
    }

    interface AxiosStatic {
      (config: AxiosRequestConfig): Promise<AxiosResponse>;
      get<T = any>(
        url: string,
        config?: AxiosRequestConfig
      ): Promise<AxiosResponse<T>>;
      post<T = any>(
        url: string,
        data?: any,
        config?: AxiosRequestConfig
      ): Promise<AxiosResponse<T>>;
      put<T = any>(
        url: string,
        data?: any,
        config?: AxiosRequestConfig
      ): Promise<AxiosResponse<T>>;
      delete<T = any>(
        url: string,
        config?: AxiosRequestConfig
      ): Promise<AxiosResponse<T>>;
    }
  }

  const axios: axios.AxiosStatic;
}

export {};
// 使用例
interface User {
  id: number;
  name: string;
  email: string;
}

async function fetchUsers(): Promise<User[]> {
  try {
    const response = await axios.get<User[]>("/api/users");
    return response.data;
  } catch (error) {
    console.error("Error fetching users:", error);
    return [];
  }
}

// ユーザー作成
async function createUser(userData: Omit<User, "id">): Promise<User | null> {
  try {
    const response = await axios.post<User>("/api/users", userData);
    return response.data;
  } catch (error) {
    console.error("Error creating user:", error);
    return null;
  }
}

複数ライブラリの組み合わせ例

// types/combined-cdn.d.ts
declare global {
  // Moment.js
  interface Moment {
    format(format?: string): string;
    add(amount: number, unit: string): Moment;
    subtract(amount: number, unit: string): Moment;
    isBefore(date: Moment | Date): boolean;
    isAfter(date: Moment | Date): boolean;
  }

  interface MomentStatic {
    (): Moment;
    (date: string | Date): Moment;
    now(): number;
  }

  const moment: MomentStatic;

  // Lodash
  interface LoDashStatic {
    map<T, U>(array: T[], iteratee: (item: T) => U): U[];
    filter<T>(array: T[], predicate: (item: T) => boolean): T[];
    sortBy<T>(array: T[], key: keyof T): T[];
    groupBy<T>(
      array: T[],
      key: keyof T | ((item: T) => string)
    ): { [key: string]: T[] };
    debounce<T extends (...args: any[]) => any>(func: T, wait: number): T;
  }

  const _: LoDashStatic;
}

export {};
// 組み合わせ使用例
interface Task {
  id: number;
  title: string;
  completed: boolean;
  createdAt: Date;
}

class TaskManager {
  private tasks: Task[] = [];

  async loadTasks(): Promise<void> {
    try {
      const response = await axios.get<Task[]>("/api/tasks");
      this.tasks = response.data.map((task) => ({
        ...task,
        createdAt: new Date(task.createdAt),
      }));
      this.renderTasks();
    } catch (error) {
      console.error("Failed to load tasks:", error);
    }
  }

  private renderTasks(): void {
    const tasksByStatus = _.groupBy(this.tasks, "completed");
    const completedTasks = tasksByStatus.true || [];
    const pendingTasks = tasksByStatus.false || [];

    $("#completed-count").text(completedTasks.length.toString());
    $("#pending-count").text(pendingTasks.length.toString());

    const taskList = $("#task-list").empty();

    _.sortBy(this.tasks, "createdAt").forEach((task) => {
      const formattedDate = moment(task.createdAt).format("YYYY-MM-DD HH:mm");
      const taskElement = $(`
        <div class="task ${task.completed ? "completed" : ""}">
          <h3>${task.title}</h3>
          <small>Created: ${formattedDate}</small>
        </div>
      `);
      taskList.append(taskElement);
    });
  }
}

// 初期化
$(document).ready(() => {
  const taskManager = new TaskManager();
  taskManager.loadTasks();
});

5. グローバルライブラリの扱い

window オブジェクトの拡張

その他のグローバルライブラリの型定義:

// types/global.d.ts
declare global {
  interface Window {
    MyGlobalLibrary: {
      init(config: any): void;
      version: string;
    };
  }
}

// 使用例
window.MyGlobalLibrary.init({ debug: true });
console.log(window.MyGlobalLibrary.version);

5. 実践的な例

React プロジェクトでの活用

// 型定義のあるライブラリ
import axios from "axios";
import moment from "moment";

// カスタム型定義が必要なライブラリ
declare module "react-custom-component" {
  interface Props {
    title: string;
    onClose?: () => void;
  }
  const Component: React.FC<Props>;
  export default Component;
}

import CustomComponent from "react-custom-component";

const App: React.FC = () => {
  return (
    <CustomComponent title="Hello" onClose={() => console.log("closed")} />
  );
};

Node.js プロジェクトでの活用

// package.json でtype: "module"を設定している場合
import express from "express";
import { readFile } from "fs/promises";

// CommonJS形式のライブラリを使用
import createDebug from "debug";
const debug = createDebug("app:main");

const app = express();

app.get("/", async (req, res) => {
  const data = await readFile("./data.json", "utf8");
  debug("Data loaded");
  res.json(JSON.parse(data));
});

6. 設定とベストプラクティス

tsconfig.json の推奨設定

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ES2020", "DOM"],
    "moduleResolution": "node",
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "typeRoots": ["./types", "./node_modules/@types"]
  },
  "include": ["src/**/*", "types/**/*"]
}

プロジェクト構造

project/
├── src/
│   ├── components/
│   └── utils/
├── types/
│   ├── global.d.ts
│   ├── custom-library.d.ts
│   └── vendors.d.ts
├── package.json
└── tsconfig.json

7. トラブルシューティング

よくある問題と解決法

問題 1: モジュールが見つからない

// エラー: Cannot find module 'some-library'
// 解決法: 型宣言を追加
declare module "some-library";

問題 2: 型の不整合

// 問題のあるコード
const result = someLibraryMethod(); // any型

// 改善されたコード
interface LibraryResult {
  success: boolean;
  data: any;
}
declare function someLibraryMethod(): LibraryResult;

問題 3: グローバル変数へのアクセス

// エラー: Property 'customLib' does not exist on type 'Window'
// 解決法
declare global {
  interface Window {
    customLib: any;
  }
}

まとめ

TypeScript で JavaScript ライブラリを使用する際は、以下の優先順位で対応することを推奨します:

  1. @types パッケージの利用: 最も安全で効率的
  2. カスタム型定義の作成: 必要な部分のみ定義
  3. any 型の使用: 最後の手段として一時的に使用

適切な型定義を設定することで、TypeScript の恩恵を最大限に活用しながら、豊富な JavaScript エコシステムを利用できます。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?