# 2026年のVue 3とTypeScript: 型安全なProps・Emits・コンポーザブル
> TypeScriptで型安全なVue 3コンポーネントを習得: ジェネリックdefineProps、タプル形式のdefineEmits、型付きコンポーザブル、defineModel、InjectionKey、面接質問まで解説。
- Published: 2026-07-07
- Updated: 2026-07-07
- Author: SharpSkill
- Tags: Vue 3, TypeScript, defineProps, Composables, Frontend
- Reading time: 8 min
---
2026年のVue 3とTypeScriptの組み合わせは、props・イベント・再利用ロジックのすべてに静的な型チェックをもたらします。`
{{ props.name }}
{{ props.role }}
```
`role` propは2つのリテラル文字列に制約されるため、`role="guest"` を渡すとビルドが失敗します。これこそがTypeScriptと `defineProps` を使う中心的な理由です。[Vue公式のTypeScriptガイド](https://vuejs.org/guide/typescript/composition-api.html)でも、`
```
Vue 3.5では**リアクティブなprops分割代入**が安定版となり、こちらがより簡潔なパターンになりました。`defineProps` の結果を分割代入し、同じ文の中でデフォルト値を割り当てても、完全にリアクティブなまま保たれます。コンパイラが内部で各アクセスを `props.x` へ書き換えるためです。
```vue
{{ label }}
```
> **分割代入したpropsのリアクティビティの罠**
>
> 分割代入したpropsはテンプレート内や `computed` の中ではリアクティブなまま保たれますが、分割代入した値をそのまま `watch` やコンポーザブルに渡すと一度だけ読み取られ、リアクティブなつながりが失われます。ゲッターで包む(`watch(() => color, ...)`)か、下流でrefが必要な場合は `toRef(props, 'color')` で変換してください。
これは面接でよくある罠です。候補者は分割代入した変数を単なる値だと思い込みますが、実際にはコンパイラがすべての読み取りを `props.color` へ配線し直しています。
## defineEmitsによる型安全なemits
イベントもpropsと同じ厳密さに値します。ジェネリック版の `defineEmits` は、各イベント名とそのペイロードをタプルとして記述し、親コンポーネントには補完を、子コンポーネントには正しい引数がemitされているかのコンパイル時チェックを提供します。
```vue
```
このタプル形式は、古い呼び出しシグネチャ構文(`(e: 'search', q: string): void`)を置き換えるものです。読みやすく、オーバーロードなしで複数のイベントに対応できるためです。イベントがデータを持たない場合、空のタプル `[]` がそれを明示的に文書化します。型付きのemitsと型付きのpropsを組み合わせることで、コンポーネントの公開インターフェース全体が実行前に検証可能になります。これは[Vue Composition APIガイド](/blog/vue-nuxt/vue-3-composition-api-complete-guide)で扱っている規律と同じものです。
## 再利用ロジックのためのコンポーザブルに型を付ける
コンポーザブルは単なる関数なので、通常のTypeScriptルールに従います。ただし、いくつかの慣習を守ると扱いやすさが保たれます。推論が自明でない場合は `Ref` 型を明示的に返し、APIレスポンスのような任意のデータをラップする場合はジェネリックを使いましょう。
```ts
// useFetch.ts
import { ref, type Ref } from 'vue'
interface UseFetchReturn {
data: Ref
error: Ref
loading: Ref
}
// Generic flows through to the caller's typed data
export function useFetch(url: string): UseFetchReturn {
const data = ref(null) as Ref
const error = ref(null)
const loading = ref(true)
fetch(url)
.then((r) => r.json())
.then((json: T) => { data.value = json })
.catch((e: Error) => { error.value = e })
.finally(() => { loading.value = false })
return { data, error, loading }
}
```
呼び出し側では、ジェネリックパラメータによって `data` が追加の注釈なしに完全に型付けされます。
```ts
// UserList.vue (script setup)
interface User { id: number; name: string }
// data is Ref — inferred from the generic
const { data: users, loading } = useFetch('/api/users')
```
戻り値オブジェクトに明示的に型注釈を付けること(`UseFetchReturn`)は、数行余分に書く価値があります。契約を文書化し、内部refの意図しない漏洩を防ぎ、利用者にインポートすべき単一の型を提供するからです。引数のオーバーロードやライフサイクルに応じたクリーンアップなど、より深いコンポーザブルパターンについては[上級Vueコンポーザブルガイド](/blog/vue-nuxt/advanced-vue-3-composables-reusable-patterns)を参照してください。
## 双方向バインディングのためのdefineModelに型を付ける
Vue 3.4以降で安定版となった `defineModel` は、従来の `modelValue` propと `update:modelValue` emitを、単一の書き込み可能なrefにまとめます。ジェネリックパラメータがバインディングの両方向を一度に型付けします。
```vue
```
名前付きモデル(`defineModel('title')`)は `v-model:title` に対応し、同じ型付けを受け取ります。これにより、手動のprop・emitパターンが隠していたペイロード不一致のバグ群を丸ごと排除できます。
## テンプレートrefとコンポーネントインスタンスに型を付ける
`ref` を通じてDOMノードや子コンポーネントにアクセスする箇所こそ、型のないVueコードが最も頻繁に `any` へ後退する場所です。解決策は、`useTemplateRef`(Vue 3.5以降)や `ref` 自体を要素型でパラメータ化することです。そうすればプロパティアクセスが実際のDOMインターフェースに対してチェックされます。
```vue
```
子コンポーネントへの参照には、`InstanceType` がコンポーネントの公開型を抽出し、子が `defineExpose` で宣言した内容を公開します。これにより、親から子へのメソッド呼び出しが推測ではなく完全にチェックされた状態に保たれます。
```vue
```
## InjectionKeyによる型安全なprovideとinject
コンポーネントツリーをまたぐ依存性注入は、キーが型情報を運ばない限り型情報を失います。`InjectionKey` は値の型をそのキーに結び付ける型付きのシンボルであり、手動キャストなしに `provide` と `inject` の同期を保ちます。
```ts
// theme-key.ts
import type { InjectionKey, Ref } from 'vue'
export interface ThemeContext {
mode: Ref<'light' | 'dark'>
toggle: () => void
}
// The key permanently associates the ThemeContext type with this symbol
export const ThemeKey: InjectionKey = Symbol('theme')
```
```ts
// provider (script setup) — value must match ThemeContext or it fails to compile
provide(ThemeKey, { mode, toggle })
// consumer — theme is inferred as ThemeContext | undefined
const theme = inject(ThemeKey)
theme?.toggle()
```
`ThemeContext` に一致しない形状の値を提供すると、注入箇所でコンパイルエラーになり、離れたコンポーネント間のズレが出荷前に捕捉されます。
## VueのTypeScriptに関するよくある面接質問
面接官は、コンパイル時の型と実行時の挙動の境界を候補者が理解しているかを探ります。よく出る質問とその的確な回答をいくつか挙げます。
- **なぜ実行時オブジェクト形式より `defineProps()` を選ぶのか?** ジェネリックpropsはユニオン型・関数シグネチャ・ネストした形状を表現でき、これらは実行時宣言では不可能です。また型と実行時定義の重複を取り除けます。
- **分割代入したpropsはリアクティブか?** Vue 3.5以降ではリアクティブです。コンパイラがアクセスを `props.x` に書き換えるためです。ただし `watch` やコンポーザブルに渡した分割代入*済みの値*は一度だけ読み取られます。ゲッターか `toRef` を使いましょう。
- **ペイロードのないemitイベントにどう型を付けるか?** 空のタプルです: `defineEmits<{ close: [] }>()`。
- **`defineModel` は何を置き換えるのか?** `modelValue` propと `update:modelValue` emitのペアを、単一の型付き書き込み可能refにまとめます。
これらを実際の問題集で練習すると、面接が評価する反射神経が磨かれます。[Vueコンポーザブル面接モジュール](/technologies/vue-nuxt/interview-questions/vue-composables)はまさにこれらのパターンを徹底演習します。ツールも重要です。CIで `vue-tsc` を実行して型エラーがマージを止めるようにし、コンポーザブルのジェネリックが絡むときは[TypeScriptハンドブック](https://www.typescriptlang.org/docs/handbook/2/generics.html)を頼りにしましょう。エディタ体験は[Vue公式のVolarツール](https://github.com/vuejs/language-tools)によって支えられており、これらのマクロを読み取ってエラーをインラインで表示します。
## まとめ
- 実行時宣言では捉えられないユニオン型・省略可能フィールド・コールバックpropを表現するために、ジェネリック版 `defineProps()` を使いましょう。
- Vue 3.5以降ではインラインのデフォルト値付きリアクティブprops分割代入を優先し、共有デフォルト値オブジェクトのほうが明確な場合にのみ `withDefaults` を使いましょう。
- 子と親の両方でペイロードがコンパイル時にチェックされるよう、タプル形式の `defineEmits` でイベントに型を付けましょう。
- コンポーザブルの戻り値型を明示的に注釈し、ジェネリックで呼び出し側が指定したデータ型を端から端まで受け渡しましょう。
- 双方向バインディングには `defineModel()` を採用し、prop・emitの定型句を単一の型付きrefにまとめましょう。
- 型のリグレッションが本番に届く前にビルドを失敗させるため、継続的インテグレーションで `vue-tsc` を実行しましょう。
---
Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack.
HTML version of this page: https://sharpskill.dev/ja/blog/vue-nuxt/vue-3-typescript-type-safe-props-emits-composables