言語を切り替える
テーマを切り替える

Vue 3 + TypeScriptベストプラクティス:2025年版エンタープライズアーキテクチャ

はじめに

先月、チームで技術選定会議をした時のことです。プロダクトマネージャーが退室した後、フロントエンドエンジニア同士で激論になりました。「Vuexを使い続けるか」「Piniaに移行するか」「フォルダ構成はどうするか」「型定義はどこまで厳密にするか」。

正直に言うと、私が初めてVue 3 + TypeScript環境を構築した時は、tsconfig.json の設定だけで半日潰しました。「完璧だ」と思っても、別のPCでcloneすると動かなかったり。あの絶望感、経験したことがある方も多いのではないでしょうか。

この記事は、私たちのチームが数々の失敗を経て辿り着いた「2025年時点での最適解」です。小規模な個人開発から中規模以上のエンタープライズ開発まで耐えうる、堅牢なアーキテクチャを紹介します。

2025年のVue 3技術スタック選定

結論から言うと、現在のデファクトスタンダードは以下の通りです:
Vite + Vue 3 + TypeScript + Pinia + Vue Router 4

Viteの高速なHMR(ホットモジュールリプレースメント)はもはや必須です。Vue 3.6のVapor Mode(仮想DOMなしで直接DOM操作するモード)も期待されていますが、まずは安定したこのセットアップを推奨します。

なぜPiniaなのか?

Vuexユーザーの多くがPiniaへの移行をためらいますが、APIの簡潔さを見れば戻れなくなります。

// ❌ Vuex (冗長)
const store = createStore({
  state: () => ({ count: 0 }),
  mutations: {
    increment(state) { state.count++ }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => commit('increment'), 1000)
    }
  }
})

// ✅ Pinia (シンプル)
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const increment = () => count.value++
  // 以前のactions相当。普通の関数でOK
  const incrementAsync = () => setTimeout(increment, 1000)
  
  return { count, increment, incrementAsync }
})
1.5KB
Piniaのバンドルサイズ

プロジェクト・ディレクトリ構造

「src直下にファイルを全部置く」のは卒業しましょう。かといって細かすぎても迷子になります。
私たちが採用しているのは「機能ドメイン」による分割です。

src/
├── api/                  # API通信層
│   ├── modules/          # 業務モジュールごと
│   │   ├── user.ts
│   │   └── order.ts
│   └── index.ts
├── assets/               # 静的リソース
├── components/           # 汎用コンポーネント
│   ├── base/             # ボタン、入力フォームなど原子コンポーネント
│   └── business/         # 業務共通コンポーネント
├── composables/          # 状態を持つロジック(Hooks)
│   ├── useAuth.ts
│   └── useRequest.ts
├── utils/                # 純粋なユーティリティ関数
├── layouts/              # レイアウト
├── router/               # ルーティング設定
├── stores/               # Piniaストア
├── types/                # グローバル型定義
├── views/                # ページコンポーネント
│   ├── user/
│   └── order/
├── App.vue
└── main.ts

重要なポイント: composablesutils を明確に分けること。

  • composables: refcomputed を使う「状態を持つロジック」。
  • utils: 純粋な関数(日付フォーマット、バリデーションなど)。

TypeScript型定義のベストプラクティス

tsconfig.json の設定はこれで決まりです。

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "jsx": "preserve",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "lib": ["ESNext", "DOM"],
    "skipLibCheck": true,
    "noEmit": true,
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

strict: true は絶対にONにしてください。最初はエラーが出て面倒ですが、バグを未然に防ぐコストの方が安いです。

Vueコンポーネントでの型定義

<script setup lang="ts"> 内でのProps定義が非常に快適になりました。

<script setup lang="ts">
// Props型定義
interface Props {
  title: string
  count?: number
  items: string[]
}

// マクロを使って定義(withDefaultsで初期値設定)
const props = withDefaults(defineProps<Props>(), {
  count: 0,
  items: () => []
})

// Emits定義
const emit = defineEmits<{
  update: [value: string]
  delete: [id: number]
}>()
</script>

Pinia実践ガイド

Piniaを使う際は、Composition APIスタイルsetup関数スタイル)を強く推奨します。Options APIスタイルよりも柔軟で、Vueコンポーネントと同じメンタルモデルで書けるからです。

// stores/modules/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
  // state
  const token = ref<string>('')
  
  // getters
  const isLoggedIn = computed(() => !!token.value)
  
  // actions
  const setToken = (newToken: string) => {
    token.value = newToken
    localStorage.setItem('token', newToken)
  }

  return { token, isLoggedIn, setToken }
}, {
  persist: true // pinia-plugin-persistedstateによる永続化
})

注意点: ストアから値を解凍(destructure)する時は storeToRefs を使いましょう。そうしないとリアクティビティが失われます。

// ❌ ダメな例
const { isLoggedIn } = useUserStore() // 反応しなくなる

// ✅ 正しい例
const { isLoggedIn } = storeToRefs(useUserStore())

Vue Router 4:型安全なルーティング

meta フィールドに型をつけることで、IDEの補完を効かせることができます。

// router/index.ts
import 'vue-router'

declare module 'vue-router' {
  interface RouteMeta {
    title?: string
    requiresAuth?: boolean
    roles?: string[]
  }
}

// ...ルーティング設定

コード品質と自動化(ESLint 9 & Prettier)

ESLint 9から設定ファイル形式が flat config に大きく変わりました。
unplugin-auto-import を導入すると、refcomputed をいちいちimportしなくて済み、開発体験が向上します。

// vite.config.ts
import AutoImport from 'unplugin-auto-import/vite'

export default defineConfig({
  plugins: [
    AutoImport({
      imports: ['vue', 'vue-router', 'pinia'],
      dts: 'src/auto-imports.d.ts', // 型定義ファイルを自動生成
    })
  ]
})

まとめ

技術選定に「絶対の正解」はありませんが、「定石」はあります。
今回紹介した構成は、保守性、開発速度、品質のバランスが取れた、現時点でのベストプラクティスです。
ぜひあなたの次のプロジェクトで試してみてください。

2025年版Vue 3プロジェクト構築完全フロー

Vite + TS + Pinia + ESLint 9による企業級プロジェクトのセットアップ手順

⏱️ Estimated time: 1 hr

  1. 1

    Step1: プロジェクト初期化

    npm create vite@latest my-project -- --template vue-ts
    cd my-project
    npm install
  2. 2

    Step2: コアライブラリのインストール

    npm install pinia vue-router@4
    npm install -D sass
  3. 3

    Step3: ディレクトリ構造の整備

    src/api, src/stores, src/views, src/composables, src/utils などのフォルダを作成。
    機能ごとの責務を明確にする。
  4. 4

    Step4: TypeScript設定 (tsconfig.json)

    "strict": true, "moduleResolution": "bundler", "paths": { "@/*": ["./src/*"] } を設定。
  5. 5

    Step5: Piniaセットアップ

    main.tsでcreatePinia()を使用。
    永続化が必要なら pinia-plugin-persistedstate を導入。
  6. 6

    Step6: 自動化ツールの導入

    unplugin-auto-import でAPIの自動インポートを設定。
    ESLint 9 + Prettier でコード整形ルールを統一。

FAQ

Vuexを使ってはいけないのですか?
既存のプロジェクトで動いているなら無理に変える必要はありません。しかし、Vuex 5の開発は停止しており、Vue公式もPiniaを推奨しています。新規プロジェクトではPiniaを選ぶべきです。Piniaの方が型推論が強力で、コード量も削減できます。
UtilityとComposableの違いは?
Utility(utilsフォルダ)は、入力を受け取って出力を返すだけの「純粋関数」です(例:日付フォーマット)。Composable(composablesフォルダ)は、Vueのリアクティブシステム(ref, computedなど)を使用し、状態を持つロジックを再利用可能な形で切り出したものです。
ESLint 9の設定が難しいです
Flat Configは概念が新しいため戸惑うかもしれませんが、公式の移行ガイドに従えば問題ありません。基本的には eslint.config.js ファイルに配列形式で設定を記述します。

2 min read · 公開日: 2025年11月24日 · 更新日: 2026年1月22日

コメント

GitHubアカウントでログインしてコメントできます

関連記事