Vue 3 + TypeScript のベストプラクティス:ジェネリック `defineProps`、コンポーネント型、Volar、および `tsconfig`
TypeScriptは、エンタープライズレベルのVue 3プロジェクトにおける標準です。型安全性、IDEの自動補完機能、そしてリファクタリングに対する安心感を提供します。Vue 3.4以降における<script setup lang="ts">は、TypeScriptとの統合を新たな高みへと引き上げます。具体的には、definePropsおよびdefineEmitsの自動推論、そしてコンポーネントの ref 型の完全なサポートが実現されています。
VueとTypeScriptを習得することは、初心者から上級開発者へとステップアップするための鍵となります。このコースでは、Vue 3とTypeScriptに関する包括的な知識を身につけることができます。
1. 学習内容
<script setup lang="ts">基礎- ジェネリクスを使って
definePropsを書く5つの方法 - defineEmits の型およびパラメータの制約を定義する
- ref / reactive / computed 型の推論
- コンポーネント参照型(useTemplateRef)
- Volar の詳細設定
- tsconfig.json のベストプラクティス
- TSでよくある5つのエラー
2. JSプロジェクトにおける「undefined is not a function」という悪夢
(1) 課題:JavaScript プロジェクトで発生した 100 件の「未定義」エラー
Aliceの管理機能は、もともとJSで実装されていました。よくあるバグ:
JS
// ❌ The "Flip" Version:JS Errors are not detected until runtime
export default {
props: {
user: { type: Object, required: true }
// Misspelled prop name: userName (No errors)
// prop Wrong type:No errors
// emit The event name is incorrect:No errors
}
}
VUE
<!-- The parent component uses userName,However, the child component defines user -->
<UserCard userName="Alice" /> <!-- ❌ I didn't realize it until runtime -->
<!-- emit The event name is incorrect -->
<Child @updae="handler" /> <!-- ❌ I didn't realize it until runtime -->
100件以上の潜在的なバグが実行時にのみ明らかになるため、デバッグに多大なコストがかかる。
(2) Vue 3 + TypeScript による解決策
VUE
<!-- Child component:UserCard.vue -->
<script setup lang="ts">
interface User {
id: number
name: string
email: string
}
const props = defineProps<{
user: User
variant?: 'primary' | 'secondary'
}>()
const emit = defineEmits<{
select: [userId: number]
delete: [userId: number]
}>()
</script>
VUE
<!-- Using Parent Components:An error occurs during compilation -->
<UserCard :user="alice" /> <!-- ✅ Compile-Time Type Checking -->
<UserCard @updae="handler" /> <!-- ❌ TS Error: The event does not exist. -->
すべてのエラーはコンパイル時に検出され、IDEでは赤色で強調表示されます。
(3) 収益
TypeScript を追加した後:
- 実行時エラー:80% → 10%(コンパイル時に捕捉される)
- IDEのオートコンプリート:95%以上の精度
- 自信を取り戻す:型安全性――何かを壊してしまう心配がない
- チームでの共同作業:わかりやすいインターフェースと、コミュニケーションの負担が少ないこと
3. TypeScriptの基本設定
(1) スクリプトの設定 lang="ts"
VUE
<template>
<p>{{ count }}</p>
<button @click="increment">+</button>
</template>
<script setup lang="ts">
import { ref } from 'vue'
// ✅ TS Automatic Inference:Ref<number>
const count = ref(0)
// ✅ Parameters and Return Types
function increment(): void {
count.value++
}
</script>
(2) tsconfig.json の基礎
JSON
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
]
}
(3) おすすめの構成トップ5
JSON
{
"compilerOptions": {
// 1. Strict Mode(Guaranteed to Open)
"strict": true,
// 2. No implicit any
"noImplicitAny": true,
// 3. Strict Null Checks
"strictNullChecks": true,
// 4. Strict Function Types
"strictFunctionTypes": true,
// 5. Strictly Bounded Calls
"strictBindCallApply": true
}
}
4. ジェネリクスを使って defineProps を書く 5 つの方法
(1) 方法 1:プリミティブ型
TS
const props = defineProps<{
name: string
age: number
active: boolean
}>()
(2) 方法 2:オプション + デフォルト値
TS
// withDefaults Provide a default value
const props = withDefaults(defineProps<{
name: string
age?: number
variant?: 'primary' | 'secondary'
}>(), {
age: 18,
variant: 'primary'
})
(3) アプローチ 3:複合オブジェクト/配列
TS
interface User {
id: number
name: string
email: string
}
const props = defineProps<{
user: User
items: User[]
config: Record<string, unknown>
}>()
(4) アプローチ 4:prop 関数
TS
const props = defineProps<{
formatter: (value: number) => string
onChange: (value: string) => void
}>()
(5) アプローチ 5:汎用コンポーネント
TS
// Generic Components:List<T> Can be specified item Type
<script setup lang="ts" generic="T extends { id: number }">
defineProps<{
items: T[]
selected?: T
}>()
</script>
<!-- Usage -->
<List :items="users" /> <!-- T = User -->
<List :items="products" /> <!-- T = Product -->
5. defineEmits タイプ
(1) イベント文の5つの種類
TS
// 1. Simple Events
const emit = defineEmits<{
click: []
submit: []
}>()
// 2. Single parameter
const emit = defineEmits<{
select: [id: number]
delete: [id: number]
}>()
// 3. Multi-parameter
const emit = defineEmits<{
change: [id: number, oldValue: string, newValue: string]
}>()
// 4. Optional Parameters
const emit = defineEmits<{
search: [query?: string]
load: [id: number, options?: object]
}>()
// 5. void Return Value
const emit = defineEmits<{
success: [data: object]
error: [message: string]
}>()
(2) トリガーイベント
TS
const emit = defineEmits<{
select: [id: number]
delete: [id: number]
}>()
// ✅ TypeScript Check Parameter Types
emit('select', 123) // ✅ OK
emit('select', 'abc') // ❌ TS Error
emit('delete', 456)
(3) 親コンポーネントの使用
VUE
<template>
<!-- ✅ TS Check Event Name -->
<Child @select="handleSelect" @delete="handleDelete" />
<!-- ❌ TS Error:The event does not exist. -->
<!-- <Child @updae="handler" /> -->
</template>
<script setup lang="ts">
function handleSelect(id: number) {
console.log('Selected:', id)
}
</script>
6. ref / reactive / computed 型
(1) ref の型推論
TS
import { ref } from 'vue'
// Automatically inferred as Ref<number>
const count = ref(0)
count.value = 1 // ✅
// Inferred as Ref<string>
const name = ref('Alice')
// Inferred as Ref<number | undefined>(It could be undefined)
const maybeNumber = ref<number>()
maybeNumber.value // type: number | undefined
(2) リアクティブ型推論
TS
import { reactive } from 'vue'
// Automatic Inference
const state = reactive({
count: 0,
user: { name: 'Alice', age: 25 }
})
state.count // type: number
state.user.name // type: string
// Explicit Type
interface State {
count: number
items: string[]
}
const s = reactive<State>({
count: 0,
items: []
})
(3) 計算型
TS
import { ref, computed } from 'vue'
const count = ref(10)
// Automatic Inference:ComputedRef<number>
const double = computed(() => count.value * 2)
// Explicit Type
const formatted = computed<string>(() => `Count: ${count.value}`)
7. コンポーネントの参照型(useTemplateRef)
(1) バージョン 3.5 以降 useTemplateRef の表示
VUE
<template>
<input ref="usernameInput">
<MyChart ref="chartComponent" :data="chartData" />
</template>
<script setup lang="ts">
import { useTemplateRef, onMounted } from 'vue'
import MyChart from './MyChart.vue'
// ✅ TS Automatic Inference:Ref<HTMLInputElement | null>
const inputRef = useTemplateRef<HTMLInputElement>('usernameInput')
// ✅ Component Instance Types
const chartRef = useTemplateRef<InstanceType<typeof MyChart>>('chartComponent')
onMounted(() => {
inputRef.value?.focus() // TS Auto-Complete
chartRef.value?.refresh()
})
</script>
(2) Vue 3.4—旧構文
TS
// Old notation:Must be done manually ref<>
import { ref, onMounted } from 'vue'
import MyChart from './MyChart.vue'
const inputRef = ref<HTMLInputElement | null>(null)
const chartRef = ref<InstanceType<typeof MyChart> | null>(null)
8. Volar の詳細設定
(1) インストール
BASH
# VS Code Install "Vue - Official" Extensions(Volar)
# Search:Vue - Official
(2) settings.json の推奨設定
JSON
{
"vue.enabled.volar": true,
"vue.compilerOptions.target": 3.4,
"vue.complete.casing.tags": ["PascalCase", "snake_case"],
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.preferences.includePackageJsonAutoImports": "on",
"editor.formatOnSave": true,
"[vue]": {
"editor.defaultFormatter": "Vue.volar"
}
}
(3) 掌側の5つの主要な特徴
| 機能 | 説明 |
|---|---|
| 型推論 | プロップ、エミット、およびrefに対する完全推論 |
| オートコンプリート | コンポーネント名、プロパティ、エミットに対するスマートな候補表示 |
| エラーチェック | コンパイル時に表示されるエラー(例:propの型が正しくないなど) |
| 定義へジャンプ | F12 コンポーネントの定義へジャンプ |
| リファクタリングのサポート | プロパティ名を変更すると、すべての参照が自動的に同期される |
9. 完全な例:TSの5つの主要なパターン
(1) ▶ サンプル:. definePropsの書き方5選
TS
// 1. Basics
defineProps<{ name: string }>()
// 2. Optional+Default
withDefaults(defineProps<{ name?: string }>(), { name: 'Guest' })
// 3. Complex
defineProps<{ user: User; items: User[] }>()
// 4. Function
defineProps<{ onClick: () => void }>()
// 5. Generics
defineProps<{ items: T[] }>() // Needs generic="T"
(2) ▶ サンプル:. defineEmits で 5 つのイベントを定義する
TS
// 1. Simple
defineEmits<{ click: [] }>()
// 2. Single Parameter
defineEmits<{ select: [id: number] }>()
// 3. More information
defineEmits<{ change: [old: string, new: string] }>()
// 4. Optional
defineEmits<{ search: [q?: string] }>()
// 5. void
defineEmits<{ done: [] }>()
(3) ▶ サンプル:. TSでよくある5つのエラー
TS
// 1. Property 'x' does not exist
// → Check Spelling,or add a type
// 2. Argument of type 'X' is not assignable
// → Type mismatch,Check Parameter Types
// 3. Type 'X' is not assignable to type 'Y | null'
// → Strict Null Checks, Add ! or ?
// 4. Cannot find module './X'
// → Path error,Check Import
// 5. Object is possibly 'undefined'
// → Optional Chain ?.
(4) ▶ サンプル:. 5つの主要なパフォーマンス比較
| パターン | 型安全性 | パフォーマンス | 適用性 |
|---|---|---|---|
| JS | ❌ | ⭐⭐⭐⭐⭐ | プロトタイプ/小規模プロジェクト |
| TS(基本) | ⭐⭐⭐ | ⭐⭐⭐⭐ | 総合 |
| TS (Strict) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | エンタープライズプロジェクト |
| TS + Volar | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | おすすめ |
| TS + tsc ビルド | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 大規模プロジェクト |
(5) ▶ サンプル:. Vue 3 + TS プロジェクトの主な構造 5 選
TEXT
src/
├── components/ # Public Components
│ ├── UserCard.vue # <script setup lang="ts">
│ └── BaseButton.vue
├── views/ # Page Components
├── stores/ # Pinia(TS)
├── composables/ # Composables
├── types/ # Type Definitions
│ ├── user.ts # export interface User
│ ├── api.ts # export interface ApiResponse<T>
│ └── index.ts # Batch Export
├── utils/ # Utility Functions
├── router/ # Routing(TS)
├── App.vue
└── main.ts # Entrance
(6) ▶ サンプル:. よくある間違いのクイックリファレンス
| エラー | 症状 | 解決策 |
|---|---|---|
| プロパティ名のスペルミス | コンパイルエラー | IDE を使って定義箇所に移動 |
| 「emit」のスペルミス | コンパイルエラー | 「defineEmits」型を使用 |
| 型変換に失敗しました | コンパイルエラー | 型アサーションまたはジェネリックを使用してください |
| ref.value が未定義 | 実行時 | オプションのチェーン ?. |
| 循環参照 | コンパイルエラー | 型のみのインポートを使用してください |
❓ よくある質問
Q Vue 3 + TS は JS よりもパフォーマンスが劣りますか?
A 型チェックはコンパイル時に実行され、実行時には影響しません(型情報は消去されます)。Volar のコンパイルは非常に高速であるため、その差はほとんど気になりません。
Q ジェネリック
defineProps とランタイム宣言、どちらが良いですか?A ジェネリックの使用をお勧めします。TypeScript はプロパティの型を自動的に推論し、IDE も優れたオートコンプリート機能を提供します。ランタイム宣言の場合、バリデータを手動で記述する必要があります。
Q 汎用コンポーネントはどのように記述しますか?
A
<script setup lang="ts" generic="T">、次に defineProps<{ items: T[] }>() と記述します。Vue 3.3 以降でサポートされています。Q TypeScriptで
defineModelはどのように記述しますか?A
const modelValue = defineModel<string>('modelValue', { default: '' })です。ジェネリックを使用して型を指定します。Q
useTemplateRef には Vue 3.5 以降が必要ですか?A はい。Vue 3.4 以前の場合は、
ref<HTMLInputElement | null>(null) を使用してください。Q tsconfig.json で厳格モードを有効にする必要がありますか?
A エンタープライズプロジェクトでは必須です。初心者は、まず厳格モードを無効にして始め、徐々に有効にしていくことをお勧めします。推奨:
"strict": true, "noImplicitAny": true, "strictNullChecks": true。Q Volcano / vue-tsc とは何ですか?
A vue-tsc は、Vue プロジェクト向けの TypeScript 型チェックツールです(Volar チームによって開発されました)。
npx vue-tsc --noEmit 型チェックを実行します(JS を生成せず、型チェックのみを行います)。Q Vue 3 と TypeScript を SSR で使用できますか?
A はい。Nuxt 3 には TypeScript サポートが組み込まれています。Pinia と Vue Router の両方が、ネイティブで TypeScript をサポートしています。
📖 まとめ
<script setup lang="ts">は、Vue 3 + TS の標準的なセットアップです- ジェネリクスを使って
definePropsを書く5つの方法:基本/オプション/複雑/関数/ジェネリック defineEmits型およびパラメータの制約により、イベントの有効性が保証されます- おすすめの tsconfig 設定:strict / noImplicitAny / strictNullChecks など
- useTemplateRef (Vue 3.5+) 完全な型推論
- Volarは、VS Codeにとって欠かせない拡張機能です
- よくある5つの間違い:スペルミス/タイプミス/循環参照
📝 練習問題
(1) 基礎問題(難易度:⭐)
1つのコンポーネントをJSからTSに変換する:
<script setup lang="ts">- defineProps ジェネリクス
- defineEmits の型
- コンパイルと検証は正常に完了しました
(2) 上級問題(難易度:⭐⭐)
完全なTS型システムの実装:
- 5つのインターフェース(ユーザー/製品/注文/カテゴリ/カート)
- ジェネリックを使用した
definePropsを含む 5 つのコンポーネント defineEmitsタイプを使用する 5 つのコンポーネント- tsconfig の厳格モード
- ヴォラー構成
(3) チャレンジ問題(難易度:⭐⭐⭐)
「Vue 3 + TS」の完全なプロジェクトの実装:
- 5店舗(ピニア+TS)
- 5つのコンポーザブル(TS)
- 10の構成要素(ジェネリクス+型推論)
- TSでよく見られる5つのエラーの解決策
- vue-tsc による型チェック(CI/CD)
- Volar + IDE:理想的な構成
- 自動型ドキュメント生成 (typedoc)



