Vue 3 の Composition API は、TypeScriptとの相性が非常に優れています。Vue 2 の Options API では型推論が効きにくい部分が多かったのですが、Vue 3 では defineProps・defineEmits・ref・computed などのAPIが最初から型安全に設計されています。
n
この記事では、Vue 3 + TypeScript の基礎セットアップから、Composition API の型定義パターン、Composables(カスタムフック相当)、状態管理ライブラリの Pinia、ルーティングの Vue Router まで、実務で必要な知識を体系的に解説します。React + TypeScriptやReact Hooks の型定義と合わせて読むと、フレームワーク間の型パターンの共通点と違いを理解できます。
n
Vue 3 + TypeScript のプロジェクトセットアップ(Vite)、defineProps・defineEmits の型定義方法、ref・reactive・computed・watch の型安全な使い方、Composables(再利用可能な Composition 関数)の型設計、Pinia ストアの TypeScript 型定義、Vue Router の型安全なルーティング、コンポーネント間通信のベストプラクティスまで網羅
n
Vue 3 + TypeScript のプロジェクトセットアップ
n
Vue 3 × TypeScript のプロジェクトは Vite を使って作成するのが現在のスタンダードです。Vite は高速なHMR(ホットモジュールリプレースメント)と TypeScript のネイティブサポートを備えています。
n
Vite でプロジェクトを作成する
n
# プロジェクト作成(vue-ts テンプレートを選択)nnpm create vite@latest my-vue-app -- --template vue-tsncd my-vue-appnnpm installnnpm run dev
n
または、Vue CLI を使って対話的に選択することもできます。
n
# Vue CLI でのプロジェクト作成nnpm create vue@latestn# → TypeScript: Yes を選択
n
tsconfig.json の確認
n
Vite テンプレートには tsconfig.json と tsconfig.node.json が含まれています。重要な設定を確認しておきましょう。
n
// tsconfig.jsonn{n "compilerOptions": {n "target": "ES2020",n "useDefineForClassFields": true,n "module": "ESNext",n "lib": ["ES2020", "DOM", "DOM.Iterable"],n "skipLibCheck": true,nn /* Bundler mode */n "moduleResolution": "bundler",n "allowImportingTsExtensions": true,n "resolveJsonModule": true,n "isolatedModules": true,n "noEmit": true,n "jsx": "preserve",nn /* 厳格な型チェック */n "strict": true,n "noUnusedLocals": true,n "noUnusedParameters": true,n "noFallthroughCasesInSwitch": truen },n "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],n "references": [{ "path": "./tsconfig.node.json" }]n}
n
useDefineForClassFields: true は Vue 3 では必須設定です。これがないと、クラスプロパティの初期化動作がVueのリアクティビティシステムと干渉することがあります。n
vue-tsc によるVueファイルの型チェック
n
通常の tsc はVueの .vue ファイルを処理できません。Vue専用の型チェックツール vue-tsc を使います。
n
# 型チェック実行nnpx vue-tsc --noEmitnn# package.json に登録n# "type-check": "vue-tsc --noEmit"
n
script setup と型定義の基礎
n
Vue 3.2 以降、<script setup> 構文が推奨されています。従来の setup() 関数より簡潔に書けて、TypeScript との相性も良くなっています。
n
script setup の基本構造
n
<!-- UserCard.vue -->n<script setup lang="ts">nimport { ref, computed } from 'vue'nn// ref は型を自動推論nconst count = ref(0) // Ref<number>nconst name = ref('Alice') // Ref<string>nn// 明示的に型を指定する場合nconst userId = ref<number | null>(null)nn// computed も型推論されるnconst doubleCount = computed(() => count.value * 2) // ComputedRef<number>nn// 関数nfunction increment() {n count.value++n}n</script>nn<template>n <div>n <p>{{ name }}: {{ count }}</p>n <button @click="increment">+1</button>n </div>n</template>
n
<script setup lang="ts"> の lang="ts" は必須です。これを書かないとVSCodeの型補完が動作しません。n
ref と reactive の使い分け
n
| API | 用途 | 型パターン | アクセス |
|---|---|---|---|
ref<T>() |
プリミティブ・単一値 | Ref<T> |
.value 必要 |
reactive<T>() |
オブジェクト全体 | T(そのまま) |
.value 不要 |
computed() |
派生値(読み取り専用) | ComputedRef<T> |
.value 必要 |
shallowRef<T>() |
深いリアクティビティ不要 | ShallowRef<T> |
.value 必要 |
n
import { ref, reactive } from 'vue'nn// ref: プリミティブに適切nconst count = ref(0) // Ref<number>nconst isOpen = ref(false) // Ref<boolean>nn// オブジェクトも ref で使える(.value でアクセス)nconst user = ref({ id: 1, name: 'Alice' })n// user.value.name にアクセスnn// reactive: オブジェクトを直接リアクティブにninterface UserState {n id: numbern name: stringn email: stringn}nnconst userState = reactive<UserState>({n id: 1,n name: 'Alice',n email: 'alice@example.com'n})n// userState.name でアクセス(.value 不要)
n
reactive() は TypeScript の型推論が弱い場合があります。複雑なオブジェクトには reactive<UserState>({}) のように明示的な型引数を使うことを推奨します。また、reactive に ref をネストすると自動アンラップされます(.value が不要になる)。n
watch と watchEffect の型定義
n
Vue 3 の watch と watchEffect は、リアクティブな値の変化を監視する際に使います。TypeScript では監視する値と変化前後の型が正しく推論されます。
n
watch の基本的な型定義
n
import { ref, watch } from 'vue'nnconst count = ref(0) // Ref<number>nconst name = ref('Alice') // Ref<string>nn// 単一の ref を監視(newVal・oldVal の型が自動推論される)nwatch(count, (newVal, oldVal) => {n // newVal: number, oldVal: numbern console.log(`${oldVal} → ${newVal}`)n})nn// 複数の ref を配列で監視nwatch([count, name], ([newCount, newName], [oldCount, oldName]) => {n // newCount: number, newName: stringn console.log(newCount, newName)n})nn// getter 関数で監視(reactive オブジェクトの特定プロパティなど)nconst user = ref({ id: 1, name: 'Alice', age: 30 })nwatch(n () => user.value.age,n (newAge, oldAge) => {n // newAge: number, oldAge: numbern console.log('年齢変更:', oldAge, '→', newAge)n }n)
n
watch のオプションと型
n
import { ref, watch } from 'vue'nninterface Config {n theme: 'light' | 'dark'n language: 'ja' | 'en'n}nnconst config = ref<Config>({ theme: 'light', language: 'ja' })nn// deep: true でオブジェクト内部の変化を監視nwatch(n config,n (newConfig) => {n // newConfig: Config(deep でも型推論される)n console.log('設定変更:', newConfig.theme)n },n { deep: true }n)nn// immediate: true で即時実行(初期値でも発火)nwatch(n () => config.value.theme,n (theme) => {n // theme: 'light' | 'dark'n document.body.setAttribute('data-theme', theme)n },n { immediate: true }n)nn// 監視を停止する(返り値の StopHandle を呼ぶ)nconst stop = watch(config, () => { /* ... */ })nstop() // 後で停止
n
watchEffect の型定義
n
import { ref, watchEffect } from 'vue'nnconst userId = ref<number | null>(null)nconst userData = ref<{ name: string } | null>(null)nn// watchEffect: 内部で参照したリアクティブ値を自動で追跡nconst stop = watchEffect(async () => {n if (userId.value === null) {n userData.value = nulln returnn }nn // userId.value を参照しているので、変化時に自動再実行n const response = await fetch(`/api/users/${userId.value}`)n userData.value = await response.json()n})nn// クリーンアップ(副作用の後処理)nwatchEffect((onCleanup) => {n const timer = setInterval(() => {n console.log('tick', userId.value)n }, 1000)nn // コールバック再実行前 or コンポーネント破棄時に実行n onCleanup(() => clearInterval(timer))n})
n
| watch | watchEffect | |
|---|---|---|
| 監視対象 | 明示的に指定 | 自動追跡(内部で参照した値) |
| 初回実行 | immediate: true で有効化 |
常に即時実行 |
| 変化前の値 | oldVal で取得可能 |
取得不可 |
| 用途 | 特定の値を明示的に監視 | 副作用を宣言的に書く |
n
変化前後の値(
oldVal/newVal)が必要 → watch。「このリアクティブ値を使った副作用」を書きたい場合 → watchEffect が簡潔。DOM 操作・API リクエスト・ログ記録などの副作用には watchEffect + onCleanup の組み合わせが有効です。n
defineProps と defineEmits の型定義
n
コンポーネントの props と emits の型定義は Vue 3 の TypeScript 統合の中でも特に重要です。defineProps と defineEmits を使った型定義パターンを詳しく解説します。
n
defineProps の型定義方法
n
props の型定義には2つのアプローチがあります。
n
// ① ランタイム宣言(従来のVue式)nconst props = defineProps({n title: String,n count: {n type: Number,n required: truen },n items: {n type: Array as PropType<string[]>,n default: () => []n }n})nn// ② 型引数による宣言(TypeScript推奨)ninterface Props {n title: stringn count: numbern items?: string[]n user?: {n id: numbern name: stringn }n}nnconst props = defineProps<Props>()n// props.title → stringn// props.count → numbern// props.items → string[] | undefined
n
withDefaults でデフォルト値を設定する
n
型引数スタイルの defineProps にデフォルト値を付けるには withDefaults を使います。
n
interface Props {n title?: stringn count?: numbern items?: string[]n variant?: 'primary' | 'secondary' | 'danger'n}nnconst props = withDefaults(defineProps<Props>(), {n title: 'デフォルトタイトル',n count: 0,n items: () => [], // 配列・オブジェクトはファクトリ関数でn variant: 'primary'n})nn// props.title → string(undefined にならない)n// props.variant → 'primary' | 'secondary' | 'danger'(型推論される)
n
defineEmits の型定義方法
n
// ① 配列スタイル(型なし)nconst emit = defineEmits(['update', 'delete'])nn// ② オブジェクト型スタイル(バリデーション付き)nconst emit = defineEmits({n update: (id: number, value: string) => true,n delete: (id: number) => id > 0n})nn// ③ 型引数スタイル(TypeScript推奨)nconst emit = defineEmits<{n update: [id: number, value: string] // ラベル付きタプル(Vue 3.3+)n delete: [id: number]n 'update:modelValue': [value: string] // v-model 対応n}>()nn// 使用時nemit('update', 1, '新しい値') // ✅ 型チェックありnemit('update', '1', '値') // ❌ 型エラー(id は number)
n
v-model の型定義
n
カスタムコンポーネントの v-model を TypeScript で型安全に実装する方法です。
n
<!-- MyInput.vue: v-model 対応コンポーネント -->n<script setup lang="ts">nconst props = defineProps<{n modelValue: stringn placeholder?: stringn}>()nnconst emit = defineEmits<{n 'update:modelValue': [value: string]n}>()nnfunction handleInput(event: Event) {n const target = event.target as HTMLInputElementn emit('update:modelValue', target.value)n}n</script>nn<template>n <inputn :value="modelValue"n :placeholder="placeholder"n @input="handleInput"n />n</template>
n
<!-- 親コンポーネントでの使用 -->n<script setup lang="ts">nimport { ref } from 'vue'nimport MyInput from './MyInput.vue'nnconst username = ref('')n</script>nn<template>n <!-- v-model が型安全に動作 -->n <MyInput v-model="username" placeholder="ユーザー名を入力" />n <p>入力値: {{ username }}</p>n</template>
n
Vue 3.3 から
defineProps でジェネリクスを使えるようになりました(<script setup lang="ts" generic="T">)。また、ラベル付きタプル構文(update: [id: number])が使えるようになり、emit の型が読みやすくなっています。n
テンプレートの型安全と型チェック
n
Vue 3 × TypeScript では、テンプレート内でも型チェックが有効です。vue-tsc を使うことでテンプレートの型エラーを検出できます。
n
テンプレート内のイベント型
n
<script setup lang="ts">n// イベントハンドラの型定義nfunction handleClick(event: MouseEvent) {n console.log(event.clientX, event.clientY)n}nnfunction handleInput(event: Event) {n // Event から HTMLInputElement にキャストn const target = event.target as HTMLInputElementn console.log(target.value)n}nnfunction handleSubmit(event: SubmitEvent) {n event.preventDefault()n // フォームの処理n}n</script>nn<template>n <button @click="handleClick">クリック</button>n <input @input="handleInput" />n <form @submit="handleSubmit">...</form>n</template>
n
v-for と型推論
n
<script setup lang="ts">ninterface Todo {n id: numbern title: stringn completed: booleann}nnconst todos: Todo[] = [n { id: 1, title: 'TypeScriptを学ぶ', completed: false },n { id: 2, title: 'Vueを学ぶ', completed: true }n]n</script>nn<template>n <!-- todo は Todo 型として推論される -->n <ul>n <li v-for="todo in todos" :key="todo.id">n <!-- todo.title → string(型チェックあり) -->n {{ todo.title }}n <!-- todo.completed → boolean -->n <span v-if="todo.completed">✓</span>n </li>n </ul>n</template>
n
template ref の型定義
n
<script setup lang="ts">nimport { ref, onMounted } from 'vue'nn// HTMLElement の refnconst inputEl = ref<HTMLInputElement | null>(null)nn// コンポーネントの refnimport MyModal from './MyModal.vue'nconst modalRef = ref<InstanceType<typeof MyModal> | null>(null)nnonMounted(() => {n // null チェックが必要n inputEl.value?.focus()n modalRef.value?.open()n})n</script>nn<template>n <input ref="inputEl" type="text" />n <MyModal ref="modalRef" />n</template>
n
InstanceType<typeof MyModal> パターンは、コンポーネントの ref から expose された メソッド・プロパティに型安全にアクセスするために使います。詳しくはReact Hooks の型定義の forwardRef・useImperativeHandle の解説(React版)も参考になります。n
defineExpose で公開するAPIを型安全に定義する
n
defineExpose を使うと、親コンポーネントから子コンポーネントの特定のプロパティ・メソッドにアクセスできるようにできます。
n
<!-- MyModal.vue -->n<script setup lang="ts">nimport { ref } from 'vue'nnconst isVisible = ref(false)nnfunction open() {n isVisible.value = truen}nnfunction close() {n isVisible.value = falsen}nn// defineExpose で公開するAPIを明示ndefineExpose({n open,n close,n isVisible // 読み取り専用にしたい場合は computed でn})n</script>
n
<!-- 親コンポーネント -->n<script setup lang="ts">nimport { ref } from 'vue'nimport MyModal from './MyModal.vue'nn// InstanceType で expose されたAPIの型を取得nconst modal = ref<InstanceType<typeof MyModal> | null>(null)nnfunction showModal() {n modal.value?.open() // open() は型安全にアクセスできるn}n</script>nn<template>n <button @click="showModal">モーダルを開く</button>n <MyModal ref="modal" />n</template>
n
Composables(コンポーザブル)の型設計
n
Composables は Vue 3 のリアクティビティシステムを使った再利用可能な関数です。React の Custom Hooks に相当します。TypeScript で型安全に設計する方法を解説します。
n
基本的なComposableの型定義
n
// composables/useCounter.tsnimport { ref, computed } from 'vue'nnexport function useCounter(initialValue = 0) {n const count = ref(initialValue)nn const doubleCount = computed(() => count.value * 2)nn function increment() { count.value++ }n function decrement() { count.value-- }n function reset() { count.value = initialValue }nn return {n count, // Ref<number>n doubleCount, // ComputedRef<number>n increment,n decrement,n resetn }n}nn// 使用時: 戻り値の型は自動推論されるn// const { count, increment } = useCounter(10)
n
非同期データ取得のComposable
n
非同期処理を含むComposableは、ローディング・エラー状態も含めて型定義します。非同期処理の型定義の非同期パターンも参考にしてください。
n
// composables/useFetch.tsnimport { ref, Ref } from 'vue'nninterface FetchState<T> {n data: Ref<T | null>n loading: Ref<boolean>n error: Ref<Error | null>n execute: () => Promise<void>n}nnexport function useFetch<T>(url: string): FetchState<T> {n const data = ref<T | null>(null) as Ref<T | null>n const loading = ref(false)n const error = ref<Error | null>(null)nn async function execute(): Promise<void> {n loading.value = truen error.value = nullnn try {n const response = await fetch(url)n if (!response.ok) {n throw new Error(`HTTP error! status: ${response.status}`)n }n data.value = await response.json() as Tn } catch (e) {n error.value = e instanceof Error ? e : new Error(String(e))n } finally {n loading.value = falsen }n }nn return { data, loading, error, execute }n}
n
<!-- 使用例 -->n<script setup lang="ts">nimport { onMounted } from 'vue'nimport { useFetch } from './composables/useFetch'nninterface User {n id: numbern name: stringn email: stringn}nn// T = User として型推論されるnconst { data: users, loading, error, execute } = useFetch<User[]>(n 'https://api.example.com/users'n)nnonMounted(() => execute())n</script>nn<template>n <div v-if="loading">読み込み中...</div>n <div v-else-if="error">エラー: {{ error.message }}</div>n <ul v-else>n <!-- user は User 型として推論される -->n <li v-for="user in users" :key="user.id">n {{ user.name }} - {{ user.email }}n </li>n </ul>n</template>
n
ローカルストレージのComposable
n
// composables/useLocalStorage.tsnimport { ref, watch, Ref } from 'vue'nnexport function useLocalStorage<T>(n key: string,n defaultValue: Tn): Ref<T> {n // ストレージから初期値を読み込むn const storedValue = localStorage.getItem(key)n const initialValue: T = storedValuen ? (JSON.parse(storedValue) as T)n : defaultValuenn const value = ref<T>(initialValue) as Ref<T>nn // 値が変わったらストレージに保存n watch(n value,n (newValue) => {n localStorage.setItem(key, JSON.stringify(newValue))n },n { deep: true }n )nn return valuen}nn// 使用例n// const theme = useLocalStorage('theme', 'light') // Ref<string>n// const settings = useLocalStorage('settings', { fontSize: 16 }) // Ref<{fontSize: number}>
n
Pinia による型安全な状態管理
n
Pinia は Vue 3 の公式状態管理ライブラリです。Vuex より TypeScript との相性が格段に良く、型推論が自然に機能します。
n
Pinia のセットアップ
n
npm install pinia
n
// main.tsnimport { createApp } from 'vue'nimport { createPinia } from 'pinia'nimport App from './App.vue'nnconst app = createApp(App)napp.use(createPinia())napp.mount('#app')
n
Setup Store(Composition API スタイル)
n
Pinia には Options Store と Setup Store の2つのスタイルがあります。TypeScript の型推論が最も効きやすい Setup Store を推奨します。
n
// stores/userStore.tsnimport { defineStore } from 'pinia'nimport { ref, computed } from 'vue'nninterface User {n id: numbern name: stringn email: stringn role: 'admin' | 'user' | 'guest'n}nnexport const useUserStore = defineStore('user', () => {n // state: ref を使うn const currentUser = ref<User | null>(null)n const users = ref<User[]>([])n const isLoading = ref(false)nn // getters: computed を使うn const isLoggedIn = computed(() => currentUser.value !== null)n const adminUsers = computed(() =>n users.value.filter(u => u.role === 'admin')n )nn // actions: 関数を定義n async function fetchUsers(): Promise<void> {n isLoading.value = truen try {n const response = await fetch('/api/users')n users.value = await response.json() as User[]n } finally {n isLoading.value = falsen }n }nn function login(user: User): void {n currentUser.value = usern }nn function logout(): void {n currentUser.value = nulln }nn // 公開するものを明示的に返すn return {n currentUser,n users,n isLoading,n isLoggedIn,n adminUsers,n fetchUsers,n login,n logoutn }n})
n
<!-- コンポーネントでの使用 -->n<script setup lang="ts">nimport { onMounted } from 'vue'nimport { storeToRefs } from 'pinia'nimport { useUserStore } from './stores/userStore'nnconst userStore = useUserStore()nn// storeToRefs: リアクティビティを保ちながら分割代入n// ※ アクションは storeToRefs に含めないnconst { currentUser, users, isLoading, isLoggedIn } = storeToRefs(userStore)nconst { fetchUsers, login, logout } = userStorennonMounted(() => fetchUsers())n</script>nn<template>n <div v-if="isLoading">読み込み中...</div>n <div v-else-if="isLoggedIn">n ようこそ、{{ currentUser?.name }} さんn <button @click="logout">ログアウト</button>n </div>n <ul>n <li v-for="user in users" :key="user.id">n {{ user.name }} ({{ user.role }})n </li>n </ul>n</template>
n
const { currentUser } = userStore と直接分割代入すると、リアクティビティが失われてテンプレートが更新されなくなります。storeToRefs() を使うと ref として取り出されるため、リアクティビティが維持されます。ただしアクション(関数)は storeToRefs に含めず、ストアから直接取り出します。n
Pinia プラグインの型定義
n
// Pinia プラグインで永続化(pinia-plugin-persistedstate の例)nimport { createPinia } from 'pinia'nimport piniaPluginPersistedstate from 'pinia-plugin-persistedstate'nnconst pinia = createPinia()npinia.use(piniaPluginPersistedstate)nn// ストア定義でオプションを指定nexport const useSettingsStore = defineStore('settings', () => {n const theme = ref<'light' | 'dark'>('light')n const language = ref<'ja' | 'en'>('ja')n return { theme, language }n}, {n persist: true // ローカルストレージに自動保存n})
n
Vue Router の型安全なルーティング
n
Vue Router 4 は TypeScript をネイティブサポートしています。ルートパラメータや名前付きルートを型安全に扱う方法を解説します。
n
ルーター設定の型定義
n
// router/index.tsnimport { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'nnconst routes: RouteRecordRaw[] = [n {n path: '/',n name: 'home',n component: () => import('../views/HomeView.vue')n },n {n path: '/users/:id',n name: 'user-detail',n component: () => import('../views/UserDetailView.vue'),n props: true // params を props として渡すn },n {n path: '/admin',n component: () => import('../layouts/AdminLayout.vue'),n meta: { requiresAuth: true, role: 'admin' },n children: [n {n path: '',n name: 'admin-dashboard',n component: () => import('../views/AdminDashboard.vue')n }n ]n }n]nnconst router = createRouter({n history: createWebHistory(import.meta.env.BASE_URL),n routesn})nnexport default router
n
RouteMeta の型拡張
n
ルートの meta プロパティに独自の型を追加するには、型宣言ファイルを使います。
n
// src/types/vue-router.d.tsnimport 'vue-router'nndeclare module 'vue-router' {n interface RouteMeta {n requiresAuth?: booleann role?: 'admin' | 'user'n title?: stringn }n}
n
// ナビゲーションガードでの使用nrouter.beforeEach((to, _from, next) => {n // to.meta.requiresAuth は boolean | undefined として型推論されるn if (to.meta.requiresAuth) {n const isAuthenticated = checkAuth()n if (!isAuthenticated) {n next({ name: 'login' })n returnn }n }n next()n})
n
useRouter と useRoute の型安全な使用
n
<script setup lang="ts">nimport { useRouter, useRoute } from 'vue-router'nimport { computed } from 'vue'nnconst router = useRouter()nconst route = useRoute()nn// route.params は Record<string, string | string[]>n// 明示的なキャストが必要な場合nconst userId = computed(() => {n const id = route.params.idn return Array.isArray(id) ? id[0] : idn})nn// 型安全なナビゲーションnfunction goToUserDetail(id: number) {n router.push({ name: 'user-detail', params: { id } })n}nnfunction goBack() {n router.back()n}n</script>
n
unplugin-vue-router を使うと、ファイルベースルーティングと型安全なルート名・パラメータが自動生成されます。Nuxt.js 3 のルーティングと同等の型安全性をVue 3 単体でも実現できます。
n
provide / inject の型安全なパターン
n
Vue 3 の provide / inject を TypeScript で使う場合、InjectionKey を使って型安全にする必要があります。
n
// composables/useTheme.tsnimport { provide, inject, ref, Ref, InjectionKey } from 'vue'nntype Theme = 'light' | 'dark'nninterface ThemeContext {n theme: Ref<Theme>n toggleTheme: () => voidn}nn// InjectionKey で型を紐付ける(Symbol を使う)nexport const ThemeKey: InjectionKey<ThemeContext> = Symbol('theme')nn// Provider側(通常はApp.vueやレイアウトコンポーネント)nexport function useThemeProvider() {n const theme = ref<Theme>('light')nn function toggleTheme() {n theme.value = theme.value === 'light' ? 'dark' : 'light'n }nn provide(ThemeKey, { theme, toggleTheme })nn return { theme, toggleTheme }n}nn// Consumer側(子コンポーネント)nexport function useTheme() {n // inject は型が自動推論される(ThemeContext | undefined)n const context = inject(ThemeKey)n if (!context) {n throw new Error('useTheme は ThemeProvider の子コンポーネントで使用してください')n }n return contextn}
n
<!-- App.vue: Provider -->n<script setup lang="ts">nimport { useThemeProvider } from './composables/useTheme'nuseThemeProvider()n</script>nn<!-- ChildComponent.vue: Consumer -->n<script setup lang="ts">nimport { useTheme } from './composables/useTheme'nconst { theme, toggleTheme } = useTheme()n</script>nn<template>n <div :class="`theme-${theme}`">n <button @click="toggleTheme">n {{ theme === 'light' ? 'ダークモード' : 'ライトモード' }}に切り替えn </button>n </div>n</template>
n
実務で使える型定義パターン集
n
コンポーネントのProps型をエクスポートして再利用
n
<!-- ButtonComponent.vue -->n<script setup lang="ts">nexport interface ButtonProps {n label: stringn variant?: 'primary' | 'secondary' | 'danger'n size?: 'sm' | 'md' | 'lg'n disabled?: booleann loading?: booleann}nnconst props = withDefaults(defineProps<ButtonProps>(), {n variant: 'primary',n size: 'md',n disabled: false,n loading: falsen})n</script>
n
// 他のコンポーネントやComposableで再利用nimport type { ButtonProps } from './ButtonComponent.vue'nn// フォームのボタン設定などninterface FormConfig {n submitButton: ButtonPropsn cancelButton: ButtonPropsn}nnconst formConfig: FormConfig = {n submitButton: { label: '保存する', variant: 'primary', loading: false },n cancelButton: { label: 'キャンセル', variant: 'secondary' }n}
n
非同期コンポーネントとエラーハンドリング
n
// 非同期コンポーネントの型安全な定義nimport { defineAsyncComponent } from 'vue'nnconst AsyncUserList = defineAsyncComponent({n loader: () => import('./components/UserList.vue'),n loadingComponent: () => import('./components/LoadingSpinner.vue'),n errorComponent: () => import('./components/ErrorMessage.vue'),n delay: 200, // ローディング表示までの遅延(ms)n timeout: 10000 // タイムアウト(ms)n})
n
Generic コンポーネント(Vue 3.3+)
n
Vue 3.3 以降では、コンポーネント自体にジェネリクスを持たせることができます。
n
<!-- SelectComponent.vue -->n<script setup lang="ts" generic="T extends { id: number; label: string }">ninterface Props {n items: T[]n modelValue: T | nulln placeholder?: stringn}nninterface Emits {n 'update:modelValue': [value: T]n}nnconst props = withDefaults(defineProps<Props>(), {n placeholder: '選択してください'n})nnconst emit = defineEmits<Emits>()n</script>nn<template>n <selectn :value="modelValue?.id ?? ''"n @change="(e) => {n const id = Number((e.target as HTMLSelectElement).value)n const selected = items.find(item => item.id === id)n if (selected) emit('update:modelValue', selected)n }"n >n <option value="">{{ placeholder }}</option>n <option v-for="item in items" :key="item.id" :value="item.id">n {{ item.label }}n </option>n </select>n</template>
n
<!-- 使用例 -->n<script setup lang="ts">nimport { ref } from 'vue'nimport SelectComponent from './SelectComponent.vue'nninterface Category {n id: numbern label: stringn color: string // 追加プロパティn}nnconst categories: Category[] = [n { id: 1, label: 'TypeScript', color: 'blue' },n { id: 2, label: 'Vue.js', color: 'green' }n]nnconst selectedCategory = ref<Category | null>(null)n</script>nn<template>n <!-- T = Category として型推論される -->n <SelectComponentn v-model="selectedCategory"n :items="categories"n placeholder="カテゴリを選択"n />n</template>
n
よくある質問(FAQ)
n
this の型推論が複雑で、Mixins を使うと型が失われることがあります。Composition API は関数ベースのため型推論が自然に機能し、Composables による再利用も型安全に行えます。新規プロジェクトでは Composition API の使用を強く推奨します。n
tsc は通常の TypeScript ファイル(.ts)しか処理できません。vue-tsc は .vue ファイルのテンプレートも含めて型チェックできます。CI/CD パイプラインでは必ず vue-tsc --noEmit を使ってください。ただし vue-tsc は型チェックのみで、実際のコンパイルは Vite が行います。n
ref を使うことを推奨します。理由は(1).value アクセスで明示的にリアクティブな値だとわかる、(2)プリミティブとオブジェクト両方に使える、(3)Composables からの返り値として扱いやすい、の3点です。reactive はオブジェクト全体をリアクティブにしたいとき、かつ .value を書くのが煩わしい場合に使うと良いでしょう。n
n
defineAsyncComponent で遅延ロード、(2)v-memo ディレクティブで再レンダリング抑制、(3)大きなリストには v-for + :key を適切に設定、(4)shallowRef・shallowReactive で深いリアクティビティを避ける、(5)computed の積極活用(副作用なしの値は watch より computed を使う)、の5点です。n
まとめ
n
Vue 3 + TypeScript の型定義パターンをまとめます。
n
| 機能 | 推奨パターン | ポイント |
|---|---|---|
| Props定義 | defineProps<Interface>() |
withDefaults でデフォルト値 |
| Emits定義 | defineEmits<{event: [args]}>() |
ラベル付きタプル(Vue 3.3+) |
| リアクティブ値 | ref<T>() を基本に |
reactive はオブジェクト全体向け |
| 派生値 | computed() |
副作用なしなら watch より優先 |
| 再利用ロジック | Composables(use〇〇関数) | InjectionKey で型安全なprovide/inject |
| 状態管理 | Pinia(Setup Store) | storeToRefs でリアクティビティ保持 |
| ルーティング | RouteRecordRaw[] + RouteMeta拡張 |
unplugin-vue-router で更に型安全に |
| 公開API | defineExpose() |
InstanceType で親から型安全アクセス |
n
Vue 3 + TypeScript をさらに深めるには、ジェネリクス完全ガイドを読んでComposablesのジェネリクス設計を学ぶのが効果的です。また、型の絞り込み(Type Narrowing)を理解しておくと、テンプレートやComposable内での型絞り込みがスムーズになります。バックエンドとの連携は非同期処理の型定義の非同期処理パターンも参考にしてください。
n