T-CREATOR

<div />

TypeScriptでマイクロフロントエンドを設計する 分割と共有型を運用で破綻させない要点

2026年1月19日
TypeScriptでマイクロフロントエンドを設計する 分割と共有型を運用で破綻させない要点

複数チームでフロントエンドを分割開発しているプロジェクトで、「共有型の変更がいつの間にか他モジュールを壊していた」「バージョン違いで型の不整合が起きた」といった問題に悩んだ経験はないでしょうか。この記事では、TypeScript 環境でマイクロフロントエンドを設計する際に、共有型の管理とバージョン互換をどう考えるかを整理し、分割しても壊れない運用を実務視点で解説します。

共有型の管理手法と比較

マイクロフロントエンドにおける型定義ファイルの共有方法には、大きく 3 つのアプローチがあります。

手法型安全性デプロイ独立性運用コスト向いているケース
共有型パッケージ(npm)型変更頻度が低い
ビルド時型生成(Module Federation)独立デプロイ重視
Contract-First(API スキーマ)API 境界が明確

それぞれの詳細は後述しますが、実際に試したところ「どれか一つに統一」ではなく、境界の性質に応じて使い分けるのが現実的でした。

検証環境

  • OS: macOS Sonoma 14.7
  • Node.js: 24.11.0 LTS (Krypton)
  • TypeScript: 5.9.3
  • 主要パッケージ:
    • @module-federation/enhanced: 0.8.4
    • @rspack/core: 1.7.2
    • zod: 3.24.1
    • react: 19.0.0
  • 検証日: 2026 年 1 月 19 日

マイクロフロントエンドで型安全が崩れる背景

マイクロフロントエンドとは、フロントエンドを独立した小さなモジュールに分割し、各チームが個別に開発・デプロイできるアーキテクチャです。バックエンドのマイクロサービスと同様の考え方をフロントエンドに適用したものです。

TypeScript の静的型付けは、単一リポジトリでは強力に機能します。しかし、モジュールを分割して独立デプロイする構成になると、以下の問題が発生しやすくなります。

動的インポートによる型情報の欠落

Module Federation などでリモートモジュールを読み込む場合、ビルド時に型情報が存在しないことがあります。

typescript// 型情報が取得できない例
const RemoteComponent = React.lazy(
  () => import('userModule/UserProfile')
);

// props の型が any になり、型安全が失われる
<RemoteComponent userId={123} />  // 本当は string なのに number を渡してもエラーにならない

つまずきやすい点:IDE 上ではエラーが出ないため、本番環境で初めて不具合に気づくケースが多いです。

型定義のバージョン不整合

共有型パッケージを使う場合、各モジュールが異なるバージョンを参照していると、実行時に型の不整合が発生します。

typescript// shared-types@1.0.0 を使用するモジュール A
interface User {
  id: string;
  name: string;
}

// shared-types@2.0.0 を使用するモジュール B(email が必須に)
interface User {
  id: string;
  name: string;
  email: string; // 追加されたフィールド
}

// モジュール A からモジュール B にユーザー情報を渡すと
// email が undefined で実行時エラー

共有型管理が破綻する典型的な課題

ここでは、実務で実際に起きた問題とその原因を整理します。

暗黙的な破壊的変更

共有型パッケージでフィールドを追加しただけのつもりが、他モジュールの動作を壊すケースがあります。

typescript// 変更前
interface Order {
  id: string;
  items: OrderItem[];
  status: "pending" | "completed";
}

// 変更後(status に新しい値を追加)
interface Order {
  id: string;
  items: OrderItem[];
  status: "pending" | "processing" | "completed"; // processing を追加
}

型定義上は後方互換に見えますが、消費側で網羅的な switch 文を書いていた場合、processing のハンドリングが漏れてデフォルトケースに落ちることがあります。

検証の結果、このような「見かけ上の後方互換」が最も事故につながりやすいことがわかりました。

デプロイタイミングのずれ

独立デプロイがマイクロフロントエンドの利点ですが、型の整合性という観点ではリスクになります。

以下の図は、デプロイタイミングのずれによって型不整合が発生する流れを示しています。

mermaidsequenceDiagram
    participant Types as 共有型
    participant ModA as モジュール A
    participant ModB as モジュール B
    participant User as ユーザー

    Types->>Types: v2.0.0 リリース
    ModA->>ModA: v2.0.0 で再ビルド・デプロイ
    Note over ModB: まだ v1.0.0 のまま
    User->>ModA: アクセス
    ModA->>ModB: データ連携
    ModB-->>User: 型不整合でエラー

この図のように、共有型の更新後にすべてのモジュールが同時にデプロイされるとは限りません。

CI/CD パイプラインの複雑化

共有型パッケージを更新するたびに、依存するすべてのモジュールを再ビルド・再デプロイする必要があると、CI/CD の設計が複雑になります。

業務で問題になったのは、共有型の軽微な修正でも全モジュールのデプロイが走り、リリースサイクルがモノリス時代より遅くなったケースでした。

型共有の 3 つの手法と判断基準

ここからは、共有型の管理手法を詳しく比較し、どの場面でどの手法を選ぶべきかを解説します。

共有型パッケージ(npm パッケージ)

型定義を専用の npm パッケージとして管理し、各モジュールが依存として参照する手法です。

typescript// @company/shared-types パッケージ
export interface User {
  id: string;
  name: string;
  email: string;
  role: UserRole;
}

export type UserRole = "admin" | "member" | "guest";

export interface ApiResponse<T> {
  success: boolean;
  data: T;
  errors?: ApiError[];
}
json// 各モジュールの package.json
{
  "dependencies": {
    "@company/shared-types": "^2.0.0"
  }
}

メリット

  • ビルド時に完全な型チェックが効く
  • IDE の補完・ナビゲーションが機能する
  • 型の変更履歴がバージョンとして追跡できる

デメリット

  • パッケージ更新のたびに依存モジュールの再ビルドが必要
  • バージョン固定しないと意図しない更新が入る
  • プライベートレジストリの運用コストがかかる

つまずきやすい点^ でバージョン指定すると、CI 環境と本番環境で異なるバージョンが使われることがあります。

ビルド時型生成(Module Federation 2.0)

Module Federation 2.0 の動的型ヒント機能を使い、リモートモジュールの型定義をビルド時に自動生成する手法です。

typescript// rspack.config.ts
import { ModuleFederationPlugin } from "@module-federation/enhanced/rspack";

export default {
  plugins: [
    new ModuleFederationPlugin({
      name: "user_module",
      exposes: {
        "./UserProfile": "./src/components/UserProfile",
      },
      // 型定義の自動生成を有効化
      dts: {
        generateTypes: true,
        typesFolder: "@mf-types",
        consumeTypes: true,
      },
    }),
  ],
};
typescript// ホストアプリ側(型が自動的に解決される)
import UserProfile from 'user_module/UserProfile';

// props の型が正しく推論される
<UserProfile userId="123" onUpdate={handleUpdate} />

メリット

  • 各モジュールの独立デプロイを維持できる
  • 型定義パッケージの手動管理が不要
  • 実装と型定義の乖離が起きにくい

デメリット

  • Module Federation 2.0 以降が必要
  • 開発サーバー起動時にリモートモジュールが起動している必要がある
  • 型の互換性チェックは実行時まで遅延する

Contract-First 開発(API スキーマ)

Zod や JSON Schema などでスキーマを定義し、そこから TypeScript 型と実行時バリデーションを自動生成する手法です。

typescript// contracts/user.contract.ts
import { z } from "zod";

export const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1).max(100),
  email: z.string().email(),
  role: z.enum(["admin", "member", "guest"]),
});

// TypeScript 型を自動生成
export type User = z.infer<typeof UserSchema>;

// 実行時バリデーション付きの関数
export function parseUser(data: unknown): User {
  return UserSchema.parse(data);
}
typescript// 消費側での使用
import { parseUser, User } from "@company/contracts";

async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();

  // 実行時に型が検証される
  return parseUser(data);
}

メリット

  • コンパイル時と実行時の両方で型安全
  • API 境界での不整合を即座に検出できる
  • スキーマからドキュメント生成も可能

デメリット

  • スキーマ定義の学習コストがかかる
  • バリデーションのランタイムオーバーヘッドがある
  • 内部コンポーネント間の通信には過剰な場合がある

バージョン互換を壊さない設計指針

ここでは、共有型を変更する際にバージョン互換を維持するための具体的な指針を説明します。

追加は許容、削除・変更は慎重に

セマンティックバージョニングの原則に従い、フィールドの追加はマイナーバージョン、削除や型変更はメジャーバージョンで管理します。

typescript// v1.0.0
interface User {
  id: string;
  name: string;
}

// v1.1.0(フィールド追加は後方互換)
interface User {
  id: string;
  name: string;
  email?: string; // optional として追加
}

// v2.0.0(フィールド削除は破壊的変更)
interface User {
  id: string;
  displayName: string; // name を displayName に変更
  email: string; // required に変更
}

Union 型の拡張には注意

先述のとおり、Union 型への値追加は型上は後方互換でも、消費側の実装によっては破壊的になります。

typescript// 安全な拡張パターン
type OrderStatus = "pending" | "processing" | "completed" | (string & {}); // 将来の拡張を許容するエスケープハッチ

// 消費側は unknown status を考慮した実装にする
function renderStatus(status: OrderStatus) {
  switch (status) {
    case "pending":
      return "保留中";
    case "processing":
      return "処理中";
    case "completed":
      return "完了";
    default:
      return `不明 (${status})`; // フォールバック
  }
}

非推奨フィールドの段階的削除

フィールドを削除する際は、@deprecated で非推奨を示してから、次のメジャーバージョンで削除します。

typescriptinterface User {
  id: string;
  /** @deprecated displayName を使用してください */
  name?: string;
  displayName: string;
}

以下の図は、非推奨フィールドの段階的削除フローを示しています。

mermaidflowchart LR
    V1["v1.x<br/>name 使用"] --> V2["v2.x<br/>name deprecated<br/>displayName 追加"]
    V2 --> V3["v3.x<br/>name 削除"]

    V2 -.-> |移行期間| V2

この図のように、一定の移行期間を設けることで消費側の対応時間を確保できます。

分割しても壊れない CI/CD 設計

CI/CD パイプラインの設計も、マイクロフロントエンドの安定運用には欠かせません。

型互換チェックの自動化

共有型パッケージを更新する際、依存モジュールとの互換性を CI で自動チェックします。

yaml# .github/workflows/type-compatibility.yml
name: Type Compatibility Check

on:
  pull_request:
    paths:
      - "packages/shared-types/**"

jobs:
  check-compatibility:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        module: [user-module, order-module, host-app]
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "24"

      - name: Install dependencies
        run: yarn install --frozen-lockfile

      - name: Build shared-types (PR version)
        run: yarn workspace @company/shared-types build

      - name: Type check ${{ matrix.module }}
        run: yarn workspace ${{ matrix.module }} type-check

変更影響範囲の自動検出

どのモジュールが変更の影響を受けるかを自動検出し、必要なモジュールだけ再ビルドします。

typescript// tools/detect-affected.ts
import { execSync } from "child_process";

interface AffectedModule {
  name: string;
  reason: string;
}

function detectAffectedModules(changedPackage: string): AffectedModule[] {
  const dependencyGraph = JSON.parse(
    execSync("yarn workspaces info --json").toString(),
  );

  const affected: AffectedModule[] = [];

  for (const [name, info] of Object.entries(dependencyGraph)) {
    const deps = (info as any).workspaceDependencies || [];
    if (deps.includes(changedPackage)) {
      affected.push({
        name,
        reason: `${changedPackage} に依存`,
      });
    }
  }

  return affected;
}

// 使用例
const affected = detectAffectedModules("@company/shared-types");
console.log("影響を受けるモジュール:", affected);

段階的デプロイ戦略

全モジュールを一度にデプロイするのではなく、影響範囲の小さいモジュールから段階的にデプロイします。

yaml# 段階的デプロイの例
stages:
  - name: canary
    modules: [user-module]
    traffic: 5%
    duration: 30m

  - name: partial
    modules: [user-module, order-module]
    traffic: 25%
    duration: 1h

  - name: full
    modules: [user-module, order-module, host-app]
    traffic: 100%

実装例:Module Federation 2.0 と型共有

ここでは、Module Federation 2.0 を使った具体的な実装例を示します。

プロジェクト構成

rubyproject-root/
├── packages/
│   ├── host-app/           # ホストアプリ
│   ├── user-module/        # ユーザー管理モジュール
│   ├── order-module/       # 注文管理モジュール
│   └── shared-contracts/   # 共有スキーマ(Zod)
└── package.json

共有スキーマの定義

Contract-First アプローチで、Zod スキーマから型を生成します。

typescript// shared-contracts/src/user.ts
import { z } from "zod";

export const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  email: z.string().email(),
  role: z.enum(["admin", "member", "guest"]),
  createdAt: z.string().datetime(),
});

export type User = z.infer<typeof UserSchema>;

export const UserCreateRequestSchema = UserSchema.omit({
  id: true,
  createdAt: true,
});

export type UserCreateRequest = z.infer<typeof UserCreateRequestSchema>;

ユーザーモジュールの設定

typescript// user-module/rspack.config.ts
import { defineConfig } from "@rspack/cli";
import { ModuleFederationPlugin } from "@module-federation/enhanced/rspack";

export default defineConfig({
  entry: "./src/index.tsx",
  plugins: [
    new ModuleFederationPlugin({
      name: "user_module",
      filename: "remoteEntry.js",
      exposes: {
        "./UserList": "./src/components/UserList",
        "./UserProfile": "./src/components/UserProfile",
      },
      shared: {
        react: { singleton: true, requiredVersion: "^19.0.0" },
        "react-dom": { singleton: true, requiredVersion: "^19.0.0" },
        "@company/shared-contracts": { singleton: true },
      },
      dts: {
        generateTypes: true,
        typesFolder: "@mf-types",
      },
    }),
  ],
});

ホストアプリでの消費

typescript// host-app/src/App.tsx
import React, { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';

const UserList = React.lazy(() => import('user_module/UserList'));
const UserProfile = React.lazy(() => import('user_module/UserProfile'));

function App() {
  return (
    <div className="app">
      <ErrorBoundary fallback={<div>モジュール読み込みエラー</div>}>
        <Suspense fallback={<div>読み込み中...</div>}>
          <UserList onSelect={(user) => console.log(user)} />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

境界でのバリデーション

モジュール境界では、Contract-First で定義したスキーマを使って実行時バリデーションを行います。

typescript// user-module/src/services/UserService.ts
import { UserSchema, User } from "@company/shared-contracts";

export class UserService {
  async getUser(id: string): Promise<User> {
    const response = await fetch(`/api/users/${id}`);
    const data = await response.json();

    // 境界で型を検証(不整合があれば即座にエラー)
    return UserSchema.parse(data);
  }
}

共有型管理の詳細比較

改めて、3 つの手法を詳細に比較します。

観点共有型パッケージModule Federation 2.0 型生成Contract-First
型安全の範囲コンパイル時のみコンパイル時のみコンパイル時+実行時
実装と型の同期手動管理自動スキーマから生成
デプロイ独立性低い高い高い
初期導入コスト低い高い
運用コスト中(バージョン管理)中(スキーマ管理)
学習コスト低い高い
向いている境界内部コンポーネント間モジュール間API 境界

判断フローチャート

以下の図は、どの手法を選ぶかの判断フローを示しています。

mermaidflowchart TD
    Start["型共有が必要"] --> Q1{"独立デプロイ<br/>が必要?"}
    Q1 -->|Yes| Q2{"外部 API<br/>境界?"}
    Q1 -->|No| Pkg["共有型パッケージ"]
    Q2 -->|Yes| Contract["Contract-First"]
    Q2 -->|No| MF["Module Federation<br/>型生成"]

この図に従い、プロジェクトの特性に応じて手法を選択してください。

実務での使い分け

検証の結果、以下のような使い分けが効果的でした。

  • 同一チーム内のコンポーネント共有 → 共有型パッケージ
  • チーム間のモジュール連携 → Module Federation 2.0 型生成
  • 外部 API との通信 → Contract-First

一つのプロジェクト内でも、境界の性質に応じて複数の手法を併用することが多いです。

まとめ

マイクロフロントエンドにおける TypeScript の型安全は、単純に「型定義を共有すれば解決」という話ではありません。デプロイの独立性、バージョン互換、CI/CD の複雑さを総合的に考慮した設計が必要です。

本記事で解説したポイントを整理します。

  • 型共有の 3 手法(共有パッケージ、Module Federation 型生成、Contract-First)は、境界の性質に応じて使い分ける
  • バージョン互換は、追加は許容・削除は慎重、Union 型の拡張に注意、非推奨の段階的削除で維持する
  • CI/CD 設計では、型互換チェックの自動化と変更影響範囲の検出が重要

どの手法が最適かはプロジェクトの状況によって異なります。チーム構成、リリース頻度、許容できる複雑さを踏まえて判断してください。

関連リンク

公式ドキュメント

  • Module Federation - Module Federation 2.0 公式サイト
  • Rspack - Rust 製の高速バンドラー
  • TypeScript - TypeScript 公式ドキュメント
  • Zod - TypeScript ファーストのスキーマバリデーション

設計・アーキテクチャ

ビルドツール

著書

とあるクリエイター

フロントエンドエンジニア Next.js / React / TypeScript / Node.js / Docker / AI Coding

;